How can I merge HTML attributes with component data in Vue? - javascript

I'm trying to extract some repeated code into a Vue component. I want to be able to pass a list of CSS classes into the component using the class HTML attribute, and merge those with a "default class" that is defined in the component. I want the public interface of this component to resemble a standard HTML element, but if I try to use "class" as a prop Vue throws an error about using a JS keyword. If I try to use $attrs, my default class gets wiped out.
Is it possible to merge HTML attributes with local component data, and use the result in my template? Below is what I would like to achieve:
<template>
<img src="imageUrl" class="classes"
</template>
export default {
computed: {
imageUrl() { return 'urlString' },
},
classes() { return `${this.$attrs.class} default-class` }
}
And I'd expect an implementer to be able to use my component like so:
<CustomImageComponent class="class-name another-class" />
Which I'd expect to render this:
<img src="urlString" class="class-name another-class default-class" />

It already happens (automatically)
using <CustomImageComponent class="class-name another-class" />
will render<template><img src="imageUrl" class="my-default-class"/></template>
as <img src="imageUrl" class="my-default-class class-name another-class"/>
(in that order, with the in-template class first, and the passed classes after)
the issue is that if you have a nested DOM element that you want to apply it to, you cannot do that and you will have to use a prop
ie:
using <CustomImageComponent class="class-name another-class" />
will render<template><div><img src="imageUrl" class="my-default-class"/></div></template>
as <div class="class-name another-class"><img src="imageUrl" class="my-default-class"/></div>
and there's nothing you can do about that, other than use custom props.

You just need to use v-bind: or only colon(:) before the attributes to pass data as a value and that's it, Vue automatically merge classes, see link below:
https://codesandbox.io/s/30oly1z326

A couple things you could do, both with this.$el.classList.
On mounted:
mounted() {
this.$el.classList.add('default-class');
}
Computed property:
computed: {
classListWithDefault() {
return `${this.$el.classList.toString()} default-class`;
}
}

Related

vue.js 2 use scoped slots in custom element

I'll try to explain my problem.
I have a VueJS component, that uses slots in its template. For example:
App.vue:
<template>
<div>
<slot name="content" :content="somecontent"></slot>
</div>
</template>
<script>
export default {
data(){
return {
somecontent: "test"
}
}
}
</script>
And now I would like to initialize it like this:
<App>
<template #content="somecontent">
<div>{{somecontent}}</div>
</template>
</App>
The idea is that the user would be able to override a part of the widget content and still have some data that the widget provides. A real-life example would be a list where the elements are being loaded remotely and the user can override the list item template.
I know now that custom elements do not support scoped slots. Is there any other way to achieve this? The template syntax does not need to be like above.
Thanks for any info that could solve my problem.

Global JS variable to pass HTML chunk to React component

I have a piece of HTML I need to send to a React component on page load without rendering it. I'd rather not us AJAX due to cache, but I may revert to that if I can't figure this out.
On the jsp side, I have this:
<script>window.banner_data_for_desktop = "...droplet..."</script>
This contains the HTML chunk I need to pass
<div id="desktop-top-banner">
<div>
...
</div>
</div>
On the jsx side, I've tried rendering directly like this:
<div id="top_bar">{window.banner_data_for_desktop}</div>
This renders the content, but displays the div tags as a string and not output as HTML.
So then I tried using dangerouslySetInnerHTML like this:
<div id="top_bar">dangerouslySetInnerHTML={{ __html: window.banner_data_for_desktop }}</div>
This results in an error:
Invariant Violation: Objects are not valid as a React child (found: object with keys {__html}). If you meant to render a collection of children, use an array instead.
I've tried using Stringify, toString, creating a function to return the html like this:
function createMarkup() {
return {__html: window.banner_data_for_desktop};
}
All without any luck. If any one has a suggestion to render HTML from the global JS object, I would greatly appreciate it!
dangerouslySetInnerHtml should be an attribute of the tag:
<div dangerouslySetInnerHTML={{__html: "HTML CHUNK"}}></div>

Can I use vue.js v-if to check is the value exists in array

I have checkbox in form and I would like to use v-if directly to show/hide sections in accordance of selected checkbox values.
Is it possible or I should use watch: ?
It is possible. I would recommend adding a data model to the checkbox that toggles true or false. Then that will allow you to toggle the appearance of content using v-if.
Example:
<template>
<input type="checkbox" v-model="showContent" value="triggerString" />
<p v-if="showContent === 'triggerString'">Lorem ipsum</p>
</template>
<script>
export default {
data() {
return {
showContent: false
}
}
}
</script>
Generally speaking, we try not to use watch when we don't have to.
Update: Using the JSFiddle you included, this is how you would do it with just v-if
<template>
<input type="checkbox"
v-model="section"
name="section"
value="Epiphone"
id="epiphone">
<label for="epiphone">Epiphone</label>
<section v-if="section.includes('Epiphone')">
<h1>Epiphone</h1>
</section>
</template>
<script>
export default {
data() {
return {
section: []
}
}
}
</script>
Since there is two way binding occurring on the section array you created, there is no need for an additional div object since the only tracking that will occur will be in the section object.
The key thing to remember about v-if is that it can take actual JS expressions in it and not just data values. So you can extract out the logic from your watch method and simply plug it in there.
Hope this answers your question!

Angular 2 make function available in ng-content

I have a dropdown with a template like this:
<div class="dropdown-trigger" (click)="contentToggle()">
<ng-content select="dropdown-trigger"></ng-content>
</div>
<div class="dropdown-content" *ngIf="showContent">
<ng-content select="dropdown-content"></ng-content>
</div>
I'd like to be able to use contentToggle() in whatever I put in the ng-content so that I can have additional elements that can be used to close the dropdown, for example I might want a close button... What's the best way to do this?
This will do the trick:
<dropdown #dropdown>
<button dropdownTrigger (click)="dropdown.toggleDropdown()">Click me</button>
</dropdown>
You just assign a local template variable to the component which gives you access to everything the component has in it. Including the function you want to call.
Note that you should/need to also change the select bits to this:
<ng-content select="[dropdownTrigger]"></ng-content>
<ng-content select="[dropdownContent]"></ng-content>
Angular allow you to do this trick, Example:
import { Component } from '#angular/core'
#Component(){...}
export class DropdownComponent {
toggleDropdown() {...}
}
//parent.component.html
<dropdown-content #myDropdown>
<a (click)="myDropdown.toggleDropdown()">Close the dropdown</a>
</dropdown-content>
If you want to get a callback to the event I recommend you to read the Output Decorator

How to access content of slot in another child component

Following problem:
I have a Vue.js component which relies on its parent's DOM. But the moment the prop gets passed, it (this.$el) is undefined, probably because it's not yet mounted then.
My component's vue template file looks like this:
<template>
<md-card>
<md-card-content>
<ol>
<li v-for="item in headings(content)">
<a :href="`#${item.id}`">{{ item.name }}</a>
</li>
</ol>
</md-card-content>
</md-card>
</template>
<script>
export default {
props: ['content'],
methods: {
headings(content) {
// DOM element is used
// At this moment, `content` is undefined
},
},
};
</script>
The component that uses the one above includes this piece of code:
<article-index :content="this.$el"></article-index>
I thought of waiting for the parent component to be mounted, but that way I can't seem to keep the template like above, because it would always try to access the method (or variable) instantly.
How can I solve this?
Edit:
<template>
<div class="content">
<div class="left"><article-index :content="this.$el"></article-index></div>
<div class="article"><slot></slot></div>
<div class="right"><slot name="aside"></slot></div>
</div>
</template>
Here's the parent component's template. The only thing I actually need is the .article div, or the slot's contents.
You can get it using this.$slots, in the parent component's mount function you can access this.$slots and assign it to some variable which can be passed to article-index component.
Following code prints the passed slots:
Vue.component('wrapper', {
name: 'Wrapper',
template: `<div><slot></slot></div>`,
mounted () {
this.$slots.default.forEach(vnode => {
console.log(vnode)
})
}
})
Sample fiddle here.
With the help of #saurabh I was able to find out that I can access the slot I'm passing to the child directly.
But the core problem remained: The component was not mounted at that moment.
So I changed how I'm accessing the passed slot.
Instead of the parent element, I'm now passing the default slot in the parent component.
Since the slots prop is an Array of VNode objects, I cannot use any DOM methods on them. But since a VNode's elm property contains the actual DOM element, I'm using that instead.
Again, the problem: it's not mounted yet.
That's why the v-for now points to the headings data, not the method, which removed.
Instead, I added a mounted() method, which automatically gets called by Vue when the components got mounted.
When that method gets called, the slot has been mounted, so I can access their elm properties. In my case, there are multiple default slots, so the slots array has more than one items. To make it possible to call a specific querySelectorAll, I've added some functional Array magic.
Edit: Since it makes more sense to directly access querySelector on the rendered content, I'm now passing the $refs attribute instead of $slots.
Even though I only need $refs.article, if I pass it directly, I'll get undefined. By passing this.$refs as a whole, the child component can access the article ref even if it doesn't exist before mounting.
So this is my new parent component:
<template>
<div class="content">
<div class="left">
<article-index :refs="this.$refs"></article-index>
</div>
<div class="article" ref="article"><slot></slot></div>
<div class="right"><slot name="aside"></slot></div>
</div>
</template>
and the child:
<template>
<md-card>
<md-card-content>
<ol>
<li v-for="item in headings">
<a #click="scroll(item.id)" :href="hash">
{{ item.name }}
</a>
</li>
</ol>
</md-card-content>
</md-card>
</template>
<script>
import dashify from 'dashify';
export default {
props: ['refs'],
data: () => ({
headings: {},
hash: location.hash,
}),
methods: {
scroll(to) {
this.refs.article.querySelector(`#${to}`).scrollIntoView();
},
},
mounted() {
const elements = Array.from(this.refs.article.querySelectorAll('h2'));
elements.forEach(node => node.id = dashify(node.innerText));
this.headings = elements.map(node => ({
name: node.innerText,
id: node.id,
}));
},
};
</script>

Categories