My child component receives a prop called config, which is an object whose properties I display in my template or pass as props to other components:
<template>
<section class="container">
<h2 class="title">
{{ config.section_header }}
</h2>
<custom-button
v-for="(button, i) in config.section_buttons"
:key="i"
v-bind="button"
/>
</section>
</template>
The parent view component itself gets the config object from the store.
In my parent view component:
created () {
this.initializeStore()
},
computed: {
...mapState({
config: state => state.template?.section ?? null
})
}
The store is populated asynchronously via an Axios call:
export const actions = {
initializeStore ({ state, commit }, data) {
this.$axios.get('/path/to/api/endpoint')
.then((res) => {
// state object gets populated
})
}
}
Because the config prop is defined only after the store API call resolves, my component throws undefined error everywhere I'm trying to display parts of it. For example:
Cannot read property 'section_buttons' of null
How to solve this problem?
In initial rendering your data has not been available yet, so you've to add a conditional rendering everywhere you get that error :
<template>
<section class="container">
<h2 class="title" v-if="config">
{{ config.section_header }}
</h2>
<template v-if="config && config.section_buttons">
<custom-button
v-for="(button, i) in config.section_buttons"
:key="i"
v-bind="button"
/>
</template>
</section>
</template>
Related
I am trying to use slot-scopes in my component but I am not really successful on these. So in general what I am trying to do is I have a parent component and as a slot I am sending another component (child-component). In child component there is a button which changes the boolean.
So child component is:
<button #click="changeEditMode">Change edit mode</c-button>
methods: {
changeEditMode() {
this.$emit('edit-mode', true);
},
}
Parent component:
<div>
<slot :edit-mode="editMode"></slot>
</div>
props: {
editMode: {
type: Boolean,
required: true,
},
},
And here where both of them exist:
<parent-component>
<template v-slot="scope">
{{ scope.editMode }}
<child-component
#edit-mode="scope.editMode"
/>
</template>
</parent-component>
data() {
return {
editMode: false,
};
},
So I am expecting the value of {{ scope.editMode }} will change when I click the button but nothing changes. So where I am doing wrong?
If you simply want to update the editMode variable in your third component then you don't need to use scoped slot. Simply use named slots and you can update the data like this-
Parent component-
Give your slot a name "scope" where you are planning to inject the data.
<div>
<slot name="scope"></slot>
</div>
Another component-
Put your child component inside the named slot of the parent and simply listen to the emitted event from the child and update your editMode variable.
<parent-component>
<template #scope>
{{ editMode }}
<child-component
#edit-mode="editMode = $event"
/>
</template>
</parent-component>
data() {
return {
editMode: false,
};
},
Now, as in the question you are passing data to the slots (which I feels less required according to your use case), you can do it like this-
Parent component-
<div>
<slot :editMode="editMode"></slot>
</div>
data() {
return {
editMode: false,
}
}
Another component-
<parent-component>
<template #scope="scopeProps">
{{ scopeProps }}
</template>
</parent-component>
To know more about the named slots and pass data to named slots, read here- https://vuejs.org/guide/components/slots.html#named-slots
I'm working on tab component and I want to render tab labels in parent component by getting child's slot, named 'label'
In Vue 2.x I could approach that, by referring to $slots property of tab component, in Tabs.vue:
<template>
<section class="tabs">
<ul class="tabs-labels">
<li
v-for="tab in tabs"
:key="tab._uid"
:class="[{'active': tab.isActive}, 'tab-label']"
#click="selectTab(tab);"
>
{{ tab.$slots.label }}
</li>
</ul>
<div class="tabs-content">
<slot/>
</div>
</section>
</template>
<script>
export default {
name: 'Tabs',
data () {
return {
tabs: [],
};
},
mounted () {
// filter tabs in case there were additional vue components placed in slots
this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent');
},
methods: {
selectTab (selectedTab) {
// set isActive property of the tab by comparing their uids
this.tabs.forEach(tab => {
tab.isActive = (tab._uid === selectedTab._uid);
});
},
},
};
</script>
TabsContent.vue:
<template>
<div v-show="isActive" class="single-tab-content">
<slot/>
</div>
</template>
<script>
export default {
name: 'TabContent',
data () {
return {
isActive: false
};
},
};
</script>
Here, when the tab label clicked, in Tabs.vue I iterate through tabs array and setting their isActive property, comparing their uid and uid of selectedTab
But in Vue 3.x API of slots has changed, so I changed the way of getting tab contents:
from
this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent');
to
this.tabs = this.$slots.default().filter(tab => tab.type.name === 'TabContent');
but as I understand, it getting only vNodes, not actual VueComponent that rendered, so when I'm executing selectTab method
tab.isActive = (tab._uid === selectedTab._uid);
it updates only isActive properties for tabs, that were saved in tabs array, not for actual tab contents, so v-show never changes.
Is there any way to get actual rendered VueComponents from <slots>? Or maybe this approach is wrong from the beginning and I should try something else?
Edit
CodeSandboxes for both versions:
Vue 2.x -ignore the error about refering to children during render, it's a bug on CodeSandbox
Vue 3.x
Its a bit more complicated with Vue 3. You will want to look into using provide and inject. here is a good example.
https://gist.github.com/cathrinevaage/4eed410b31826ce390153d6834909436
sandbox - https://codesandbox.io/s/happy-rubin-z414h?file=/src/App.vue
The example above is using typescript however you get the idea.
(Vue 3, options API)
The problem: Components rerender when they shouldn't.
The situation:
Components are called with a prop whose value comes from a method.
The method cannot be replaced with a computed property because we must make operations on the specific item (in a v-for) that will send the value processed for that component.
The method returns an Array. If it returned a primitive such as a String, components wouldn't rerender.
To reproduce: change any parent's data property unrelated to the components (such as showMenu in the example below).
Parent
<template>
<div>
<div id="menu">
<div #click="showMenu = !showMenu">Click Me</div>
<div v-if="showMenu">
Open Console: A change in a property shouldn't rerender child components if they are not within the props. But it does because we call myMethod(chart) within the v-for, and that method returns an array/object.
</div>
</div>
<div v-for="(chart, index) in items" :key="index">
<MyComponent :table="myMethod(chart)" :title="chart.title" />
</div>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent,
},
data: function () {
return {
showMenu: false,
items: [{ value: 1 }, { value: 2 }],
};
},
methods: {
myMethod(item) {
// Remove [brackets] and it doesn't rerender all children
return ['processed' + item.value];
}
}
};
</script>
Child
<template>
<div class="myComponent">
{{ table }}
</div>
</template>
<script>
export default {
props: ['table'],
beforeUpdate() {
console.log('I have been rerendered');
},
};
</script>
<style>
.myComponent {
width: 10em;
height: 4em;
border: solid 2px darkblue;
}
</style>
Here's a Stackblitz that reproduces it https://stackblitz.com/edit/r3gg3v-ocvbkh?file=src/MyComponent.vue
I need components not to rerender. And I don't see why they do.
Thank you!
To avoid this unnecessary rerendering which is the default behavior try to use v-memo directive to rerender the child component unless the items property changes :
<div v-for="(chart, index) in items" :key="index" v-memo="[items]">
<MyComponent :table="myMethod(chart)" :title="chart.title" />
</div>
I've implemented the Tab feature using "keep-alive", like below, I want to pass "items2" data to the component when the selected currentTabComponent is 'Old', how do i make this work? is there any workaround?
<template>
<div>
<button #click="currentTabComponent = 'New'"> New </button>
<button #click="currentTabComponent = 'Old'"> Old </button>
</div>
<keep-alive>
<component :is="currentTabComponent" :items="currentTabComponent === 'New' ? items : items2"></component>
</keep-alive>
</template>
In the logic, i have,
<script>
export default {
data() {
return {
currentTabComponent: "New",
items:['one','two','three'],
items2:['five','six','seven']
}
}
}
</script>
Even if you use keep-alive props will be passed in the usual way, dynamic or not. So if there is a change in props it should reflect in the subcomponent. keep-alive specifically helps in preserving state changes when the component is not used, and not resetting the state when the component is shown again. But in both cases, props will work fine.
Check the below code:
<div id='component-data'>
<button
v-for="tab in tabs"
v-bind:key="tab"
v-on:click="currentTab = tab">
{{ tab }}
</button>
<keep-alive>
<component v-bind:is="currentTab"
:items="currentTab == 'Bags' ? bags : shirts"
class="tab"></component>
</keep-alive>
</div>
<script>
Vue.component('Bags', {
props: ['items'],
template: "<div>Showing {{ items.toString() }} items in bags.</div>"
});
Vue.component('Shirts', {
props: ['items'],
template: "<div>Showing {{ items.toString() }} items in shirts.</div>"
});
new Vue({
el: "#component-data",
data: {
tabs: ['Bags', 'Shirts'],
currentTab: 'Bags',
bags: ['Bag one', 'Bag two'],
shirts: ['Shirt one', 'Shirt two']
}
});
</script>
You should make sure that the sub-components 'New' and 'Old' have declared the items props in their component definition. Also I hope 'New' and 'Old' are the registered names of the components you are using for tabs.
I have just started using Vue and experienced some unexpected behavior. On passing props from a parent to child component, I was able to access the prop in the child's template, but not the child's script. However, when I used the v-if directive in the parents template (master div), I was able to access the prop in both the child script and child template. I would be grateful for some explanation here, is there a better was of structuring this code? See below code. Thanks.
Parent Component:
<template>
<div v-if="message">
<p>
{{ message.body }}
</p>
<answers :message="message" ></answers>
</div>
</template>
<script>
import Answers from './Answers';
export default {
components: {
answers: Answers
},
data(){
return {
message:""
}
},
created() {
axios.get('/message/'+this.$route.params.id)
.then(response => this.message = response.data.message);
}
}
</script>
Child Component
<template>
<div class="">
<h1>{{ message.id }}</h1> // works in both cases
<ul>
<li v-for="answer in answers" :key="answer.id">
<span>{{ answer.body }}</span>
</li>
</ul>
</div>
</template>
<script>
export default{
props:['message'],
data(){
return {
answers:[]
}
},
created(){
axios.get('/answers/'+this.message.id) //only worls with v-if in parent template wrapper
.then(response => this.answers = response.data.answers);
}
}
</script>
this.message.id only works with v-if because sometimes message is not an object.
The call that you are making in your parent component that retrieves the message object is asynchronous. That means the call is not finished before your child component loads. So when your child component loads, message="". That is not an object with an id property. When message="" and you try to execute this.message.id you get an error because there is no id property of string.
You could continue to use v-if, which is probably best, or prevent the ajax call in your child component from executing when message is not an object while moving it to updated.