I created a component for displaying some information in a nice looking way. This component is just a wrapper for the content, which is rendered inside the parent component. The parent component implements the child component this way:
<my-component v-for="item in items" :key="item.id">
<template slot="header">
{{ item.title }}
</template>
<template slot="meta">
<div>
<template v-if="typeof item.additionalData != 'undefined'">
{{ item.additionalData.text }}
</template>
</div>
</template>
</my-component>
Its working fine, until I want to change the data. items is a variable in the parent component and at render time, the data is parsed in the right way. When I change something inside of items after rendering, the child component doesn't recognize it. The reason is because the item.additionalData is added through an AJAX call after the component was already rendered.
The docs say
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
but it seems like this is only true at render time.
Am I not able to use my component this way or is there a solution for that?
From the docs:
Due to the limitations of modern JavaScript (and the abandonment of
Object.observe), Vue cannot detect property addition or deletion.
Since Vue performs the getter/setter conversion process during
instance initialization, a property must be present in the data object
in order for Vue to convert it and make it reactive.
So I suspect the fact that your item.additionalData field is initially undefined means that it's getting missed by Vue later on when it's added. The best solution is to make item.additionalData defined, but set to null or false. If that's not possible, consider using Vue.set(object, key, value) as outlined here.
I had a similar problem with adding a search icon to the item template.
The way I resolved it was to make (in terms of your example) additionalData a computed property which is then fully reactive. This is the basic form:
computed: {
additionalDataText() {
return item.additionalData ? item.additionalData.text : null
}
}
I'm unclear if your code shows the parent component or the child component.
As it is, the simplest change you can make requires the computed property to receive item as a parameter like so:
computed: {
additionalDataText() {
return function (item) {
return item.additionalData ? item.additionalData.text : null
}
}
}
and your template would be
<my-component v-for="item in items" :key="item.id">
<template slot="header">
{{ item.title }}
</template>
<template slot="meta">
<div>
<template v-if="additionalDataText(item)">
{{ additionalDataText(item) }}
</template>
</div>
</template>
</my-component>
This technique is from Unirgy's answer to this question: Can I pass parameters in computed properties in Vue.Js. There's a bit of controversy there about this method, but I tried it in my project and it seems to work.
Note, in the referenced question the accepted answer suggests using a method, but this does not give you the reactivity you require.
Related
in my parent render function, I am rendering a list of form inputs. They can be number, slider, text, etc, and for each, I have a custom Vue component, i.e. FormInputSlider. I receive some data from an API, and then have an array of these different inputs to render. Doing a huge if/else block in my parent's render code seems unmaintainable, so what is the best/standard way of rendering a FormInputSlider component when I encounter a "form-input-slider" in the list iteration? I'm coming from a React understanding of the world for what it's worth.
If you've the component names, you could just use the built-in component tag to render the component dynamically :
<component :is="dynamicName" />
instead of :
<template v-if="dynamicName==='abc'">
<abc/>
</template>
<template v-elseif="dynamicName==='xyz'">
<xyz/>
</template>
....
I've got the following set-up and weird render of class and name.
<component v-for="item in List"
:item="item"
:class="item.class"
:name="item.name"
</component>
let component = Vue.component('component', {
props: ['item'],
template: `<li :class="item.class">
{{ item.name }}
</li>`
}),
data: function (){
return {
List: [
{key:0, class:'someClass', name:'someName'},
]
}
},
})
Render:
<li class="someClass someClass" name="someName"> someName </li>
Where on earth comes double "someClass"? Why 'name' item from 'List' is an attribute here as it wasn't even used and v-bound as attribute? Is 'name' a somewhat reserved attribute?
Thanks beforehands :)
The problem is here:
<component v-for="item in List"
:item="item"
:class="item.class"
:name="item.name">
</component>
In this section you're binding the class to item.class and the attribute name to item.name. Vue will apply both of these to the outer element of the corresponding component.
Within that component you're then adding :class="item.class", which adds the class again.
To fix this just remove the :class="item.class" and :name="item.name" from that first template.
<component v-for="item in List"
:item="item">
</component>
It is also a little mysterious that you have List defined within the data of component but you seem to be using it within the scope of the parent component's template.
Update based on a comment:
A class can be set on the outer element in two ways. It can either come from the component itself, inside its template, or it can be set from the parent template.
Which one you choose depends on what the class does and which of the two components is responsible for that. Neither one is necessarily right or wrong.
Usually a component is responsible for controlling its own styling but a common exception to that is classes that control layout.
For example, if you have a button component then classes that decide whether the button should be red, green or blue would typically be managed by the component itself. However, if that button needs to be right-aligned within its parent container then the button component probably doesn't need to know about that. That alignment decision is controlled by the parent container and it can set a class on the button without the button needing to get involved.
I have the following template in a specific modal component:
<template>
<my-base-modal ref="BaseModal" :width="1000">
<template v-slot:header>Details</template>
<template v-slot:body>
<detail-card ref="DetailCard"></detail-card>
</template>
</my-base-modal>
</template>
It creates a base modal and overwrites the slots for header and body.
The body slot is filled with a sub component which needs to load some data.
I tried to use the following method to open and load the content of this modal:
open (id) {
this.$refs.DetailCard.load(id)
this.$nextTick(() => {
this.$refs.BaseModal.open()
})
}
But this.$refs.DetailCard is always undefined. I suspect this is because the reference DetailCard is defined inside the body slot of the <base-modal> component?
How am I supposed to trigger a function on the <detail-card> component in this example, without using EventBus or passing some props into it?
My suspicion is that at runtime, the DOM is not rendered until the BaseModal's open method is invoked. Therefore, this.$refs.DetailCard will returned undefined since the body slot of your component has not rendered with the nested component.
As you have mentioned in the comments, the fix can be as easy as ensuring that the DOM is already rendered, e.g. using v-show instead of v-if.
<el-tree :data="Project">
<span class="custom-tree-node" slot-scope="{ node, data }" v-contextmenu:contextmenu>
<el-input
placeholder="Please input"
v-model.trim="data.label"
#blur="saveOrDiscard(node, data)"
></el-input>
</span>
</el-tree>
I have used method as below, where I am updating id of treenode (which is from ajax call inserted id. I have simply changed to clearly explain problem).
But data of tree (which I have given as Project) is not updating. Next time it shows 0 value (which I have set as deault).
public saveOrDiscard(node: TreeNode<any, TreeData>, data: TreeData) {
//Following 456 is not updating in tree data.
node.data.id = 456;
}
Scoped Slots and Slot Props
el-tree is a slotted component and, by default, slots only have access to the same instance properties as the rest of the containing template. So for example, the slot does not have access to :data.
If you must access this data in the parent template, you need to use slot props and also explicitly bind the data in the slot's template. So in the el-tree component template, you bind the data to the slot like this:
<slot :data="data"></slot>
Then in the parent template, you can access data via the v-slot directive like this:
<template v-slot:default="slotProps">
{{ slotProps.data }}
</template>
The default argument refers to the slot name, which is named "default" if none is specified.
Here is a fiddle showing this behavior.
*You can read more about slots & slot props here.
Binding blur event as following worked for me.
#blur="() => saveOrDiscard(node, data)"
instead of following.
#blur="saveOrDiscard(node, data)"
How to target method inside other component?
I'm working on a project where I want to target a method inside another component. I press a button inside the dashboard component and I want to change the data inside my line-chart component (which uses vue-chartjs). How can I target this method?
Well you could target it over $refs however, this is pretty dirty, as it prodvides a pretty strict binding of your componentes.
A better solution would be to trigger an event (eventbus) or over an prop.
You can call children's method by referencing them through refs
In your example, your dashboard's template should looks like this :
<template>
<div>
<button #click="$refs.chart.yourMethod()">Call child method</button>
<line-chart ref="chart"></line-chart>
</div>
</template>