Accessing child component data vuejs - javascript

How do I access a child component data from the parent component?
I have a parent component "MissionPlanner" which i want to access the child component "ChosenHeroes" array called "chosenHeroes". I want to ultimately render a div if an element is in that array - and be able to update the array in the parent
I am not sure if i should be using emit() and how exactly to use it. I tried making a custom event "addHero" and pass that back. But i am getting errors
ChosenHeroes.vue
<template>
<div>
<select v-model="chosenHero">
<!-- placeholder value -->
<option :value="null">Select a hero</option>
<!-- available heroes -->
<option v-for="hero in heroes"
:key="hero.name"
:value="hero.name">
{{ hero.name }}
</option>
</select>
<span> </span>
<button #click="addHero(chosenHero)"
:disabled="chosenHero === null || chosenHeroes.length >= 3">Add Hero</button>
<br>
<h3>Chosen Heroes</h3>
<div class="chosen-heroes">
<div v-for="(hero, i) in chosenHeroes"
:key="hero.name">
<strong>Slot {{ i + 1 }}:</strong>
<Hero :hero="hero"
#removeHero="removeHero(hero)" />
</div>
</div>
</div>
</template>
<script>
import Hero from "./Hero";
export default {
components: {
Hero
},
props: {
"heroes": Array
},
data() {
return {
chosenHero: null,
chosenHeroes: []
};
},
methods: {
addHero(name) {
if(this.chosenHeroes.length < 3) {
this.chosenHeroes.push({ name });
this.chosenHero = null;
}
this.$emit("add-hero",this.chosenHeroes);
},
removeHero(hero) {
this.chosenHeroes = this.chosenHeroes.filter(h => h.name != hero.name);
}
}
};
</script>
HeroPlanner.vue
<template>
<div>
<!-- justice leage application begins here -->
<h1 id="jl">Justice League Mission Planner</h1>
<ul class="roster">
<h3>Roster:</h3>
<li v-for="hero in heroes"
:key="hero.name">
<!-- to do: conditionally display this span -->
<span v-if="isInList(hero.name)">✔ </span>
<span>{{ hero.name }} </span>
<span class="edit"
#click="editHero(hero)">edit</span>
</li>
<br>
<input type="text"
placeholder="new name"
v-model="newName"
v-if="isEdit"
#keyup.enter="changeName"
#blur="clear">
<br>
<span v-if="isEdit">enter to submit, click outside the box to cancel</span>
</ul>
<chosen-heroes :heroes="heroes" :chosenHeroes="chosenHeroes" #add-hero="addHero" />
</div>
</template>
<script>
import ChosenHeroes from "./components/ChosenHeroes.vue";
export default {
components: {
"chosen-heroes" : ChosenHeroes
},
data() {
return {
heroes: [
{ name: "Superman" },
{ name: "Batman" },
{ name: "Aquaman" },
{ name: "Wonder Woman" },
{ name: "Green Lantern" },
{ name: "Martian Manhunter" },
{ name: "Flash" }
],
newName: "",
isEdit: false,
heroToModify: null,
chosenHeroes: ChosenHeroes.data
};
},
methods: {
...isInList(heroName) {
return this.chosenHeroes.map(heroObject => heroObject.name).includes(heroName);
}
And here are the errors I got when i ran it:
vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in render: "TypeError: this.chosenHeroes.map is not a function"
found in
---> <MissionPlanner> at src/MissionPlanner.vue
<App> at src/App.vue
<Root>
warn # vue.runtime.esm.js?2b0e:619
vue.runtime.esm.js?2b0e:1888 TypeError: this.chosenHeroes.map is not a function
at VueComponent.isInList (webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/MissionPlanner.vue?vue&type=script&lang=js&:78)
at eval (eval at ./node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"aeb9565a-vue-loader-template"}!./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/MissionPlanner.vue?vue&type=template&id=e2c8c042&scoped=true& (app.js:946), <anonymous>:21:19)
at Proxy.renderList (vue.runtime.esm.js?2b0e:2630)
at Proxy.render (eval at ./node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"aeb9565a-vue-loader-template"}!./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/MissionPlanner.vue?vue&type=template&id=e2c8c042&scoped=true& (app.js:946), <anonymous>:19:15)
at VueComponent.Vue._render (vue.runtime.esm.js?2b0e:3548)
at VueComponent.updateComponent (vue.runtime.esm.js?2b0e:4066)
at Watcher.get (vue.runtime.esm.js?2b0e:4479)
at new Watcher (vue.runtime.esm.js?2b0e:4468)
at mountComponent (vue.runtime.esm.js?2b0e:4073)
at VueComponent.Vue.$mount (vue.runtime.esm.js?2b0e:8415)
I went through this article here on the emit() and how to emit data from child components to the parent components but I am not sure I used it properly

Change #add-hero="addHero" to #add-hero="anyMethodName"
and create a method:
anyMethodName(value) {
//do what you want with the chosenHeroes
}
value is the chosenHeroes that was passed through from the child component.
See link for example: https://forum.vuejs.org/t/passing-data-back-to-parent/1201/2

If you want to pass data from child to parent you can pass a parent's method as a prop to the child:
PARENT TEMPLATE SECTION
<child-component :dataHandler="dataHandler">
</child-component>
PARENT METHOD IN SCRIPT METHODS SECTION
dataHandler (input) {
// handle your new data in parent component
}
CHILD SCRIPT PROPS SECTION
props: ["dataHandler"]
register your prop. You can use dataHandler in child as normal method and pass there new data as argument - the method will be executed in parent, but with the data you provided as argument in the child.
The error that you are getting suggests that chosenHeroes is not an array (maybe it's undefined?).

The $emit will work when it is called, and in parent, it will be same as events (firing only when events are happened). In this case, you need the data from the child always render a div in the parent (if what I understand is correct).
It is better to use the Vuex store for your purpose. You can sync the data into the store from the child component. Since the store data is global, it is accessible from all components.

Related

Trying to avoid mutating a prop that goes 3 levels deep by using $emit

EDIT: Here's a repo I made for easier parsing.
I have a Component that lists products in a datatable. The first column of the table is a link that shows a modal with a form of the product that was clicked (using its ID). I'm using the PrimeVue library for styling and components.
<template>
<Column field="id" headerStyle="width: 5%">
<template #body="slotProps">
<ProductForm :product="slotProps.data" :show="showModal(slotProps.data.id)" />
<a href="#" #click.stop="toggleModal(slotProps.data.id)">
<span class="pi pi-external-link"> </span>
</a>
</template>
</Column>
</template>
<script>
import ProductForm from "./forms/ProductForm";
export default {
data() {
return {
activeModal: 0,
}
},
components: { ProductForm },
methods: {
toggleModal: function (id) {
if (this.activeModal !== 0) {
this.activeModal = 0;
return false;
}
this.activeModal = id;
},
showModal: function (id) {
return this.activeModal === id;
},
},
</script>
The modal is actually a sub component of the ProductForm component (I made a template of the Modal so I could reuse it). So it's 3 components all together (ProductList -> ProductForm -> BaseModal). Here's the product form:
<template>
<div>
<BaseModal :show="show" :header="product.name">
<span class="p-float-label">
<InputText id="name" type="text" :value="product.name" />
<label for="name">Product</label>
</span>
</BaseModal>
</div>
</template>
<script>
import BaseModal from "../_modals/BaseModal";
export default {
props: ["product", "show"],
components: { BaseModal },
data() {
return {};
},
};
</script>
When the modal pops up it uses the ProductForm subcomponent. Here is the BaseModal component:
<template>
<div>
<Dialog :header="header" :visible.sync="show" :modal="true" :closable="true" #hide="doit">
<slot />
</Dialog>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
header: String,
},
methods: {
doit: function () {
let currentShow = this.show;
this.$emit("showModel", currentShow)
},
},
data() {
return {
};
},
};
</script>
I'm passing the product object, and a show boolean that designates if the modal is visible or not from the first component (ProductList) all the way down through the ProductForm component and finally to the BaseModal component. The modal is a PrimeVue component called Dialog. The component actually has it's own property called "closable" which closes the modal with an X button when clicked, that is tied to an event called hide. Everything actually works. I can open the modal and close it. For some reason I have to click the another modal link twice before it opens after the initial.
The issue is when I close a modal, I get the Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "show" error. I've tried everything to emit to the event and change the original props value there, but the error persists (even from the code above) but I'm not sure if because I'm 3 components deep it won't work. I'm pretty new to using props and slots and $emit so I know I'm doing something wrong. I'm also new to laying out components this deep so I might not even be doing the entire layout correctly. What am I missing?
Well you are emitting the showModel event from BaseModal but you are not listening for it on the parent and forwarding it+listening on grandparent (ProductForm)
But the main problem is :visible.sync="show" in BaseModal. It is same as if you do :visible="show" #update:visible="show = $event" (docs). So when the Dialog is closed, PrimeVue emits update:visible event which is picked by BaseModal component (thanks to the .sync modifier) and causes the mutation of the show prop inside BaseModal and the error message...
Remember to never use prop value directly with v-model or .sync
To fix it, use the prop indirectly via a computed with the setter:
BaseModal
<template>
<div>
<Dialog :header="header" :visible.sync="computedVisible" :modal="true" :closable="true">
<slot />
</Dialog>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
header: String,
},
computed: {
computedVisible: {
get() { return this.show },
set(value) { this.$emit('update:show', value) }
}
},
};
</script>
Now you can add same computed into your ProductForm component and change the template to <BaseModal :show.sync="computedVisible" :header="product.name"> (so when the ProductForm receives the update:show event, it will emit same event to it's parent - this is required as Vue event do not "bubble up" as for example DOM events, only immediate parent component receives the event)
Final step is to handle update:show in the ProductList:
<ProductForm :product="slotProps.data" :show="showModal(slotProps.data.id)" #update:show="toggleModal(slotProps.data.id)"/>

Vue.js - no access to this.$parent when child component is inside <transition>

What I want: I have two components, the parent component (Wall.vue) and the child component (PostItem.vue). Every PostItem has a delete button. On click, a request to my API is sent and the item gets deleted from the database. Then I want to call the getPosts function of the parent component to get all the posts again (this time without the deleted post).
The Problem: Inside the child component, I have no access to the this.$parent Object (or more specific, it's just empty and doesn't contain the functions), so I can't call the getPosts-Function. When I remove the <transition-group> in the parent component that surrounds also the child-component, everything works fine.
What is the problem here?
Parent-Component (Wall.vue)
template-portion:
<template>
<div class="Wall view">
<transition-group name="wallstate">
<template v-else-if="messages">
<PostItem
v-for="(message, index) in messages"
:key="index"
:message="message"
:index="index"
class="PostItem"
/>
</template>
<h1 v-else>
Could not load messages. Please try later.
</h1>
</transition-group>
</div>
</template>
script-portion:
<script>
import { mapGetters } from 'vuex';
import { postsAPI } from '../services/posts.service.js';
import PostItem from '../components/PostItem.vue';
export default {
components: {
PostItem,
},
data() {
return {
messages: null,
};
},
methods: {
getPosts() {
///////Do stuff
}
}
};
</script>
Child-Component (PostItem.vue)
template-portion
<template>
<div class="PostItem__message frosted">
<p class="PostItem__messageContent">{{ message.content }}</p>
<p>
by: <strong>{{ message.user.username }}</strong>
</p>
<a
#click="deletePost"
:data-id="message._id"
v-if="message.user._id === user.id"
>
Delete
</a>
</div>
</template>
script-portion:
<script>
import { postsAPI } from '../services/posts.service.js';
import { mapGetters } from 'vuex';
export default {
name: 'PostItem',
props: {
message: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
...mapGetters({
user: 'auth/user',
}),
},
methods: {
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$parent.getPosts(); <-------- PROBLEM HERE
});
},
},
};
</script>
It's generally considered a bad practice to use this.$parent (it couples the components and reduces encapsulation / code clarity.) The child component should emit an event when it wants to send information to an ancestor component.
Remove the direct access and $emit an event called 'deleted':
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$emit('deleted'); // Emitting the event
});
},
The parent should listen for that deleted event and run an event handler:
<PostItem
v-for="(message, index) in messages"
:key="index"
:message="message"
:index="index"
class="PostItem"
#deleted="getPosts"
/>
The parent will call the getPosts method when triggered by the #deleted event listener.
inside the methods part, instead of :
methods: {
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$parent.getPosts();
});
},
},
you may try this:
methods: {
deletePost(e) {
const id = e.target.dataset.id;
let self=this;
postsAPI.removeOne(id).then((res) => {
self.$parent.getPosts();
});
}
Because of the scope chain, 'this' inside .then() does not point to the same variable environment as variable 'self' does. So perhaps it's the reason it fails to work.

Passing data from parent to child via props in v-for in VueJS 2

I went through similar questions but I couldn't find the answer and none of them worked for me.
Vuejs v-for Passing data Parent to Child
Vue - Passing parent data with same key name in v-for to child component
vue js how to pass data from parent, v-for loop list to child component with method
Pass data object from parent to child component
Vue.js - Pass in Multiple Props to Child in V-For
VUE / VUEX: How To Pass Data From Parent Template To Child Template
Parent.vue
<div class="col-sm-2" v-for="(item,index) in itemsData" :key="index">
<ItemWidget :item="item" />
</div>
<script>
import { mapState, mapActions } from "vuex";
import ItemWidget from "#/components/item/ItemWidget";
export default {
components: { ItemWidget },
computed: {
...mapState("item", ["itemsData"])
},
created() {
this.getItemList();
},
methods: {
...mapActions("item", ["getItemListX"]),
getItemList() {
this.getItemListX();
}
}
};
</script>
ItemWidget.vue
<template>
<div class="label">
<div class="label-value">{{ item.code }}</div>
</div>
</template>
<script>
export default {
props: ["item"]
};
</script>
itemsData is taken from the vuex by using MapState to the parent and itemsData is populated using created() method in the parent via an axios call in the vuex.
Error 1.
[Vue warn]: Error in render: "TypeError: _vm.item is undefined"
Error 2.
TypeError: "_vm.item is undefined"
How can I fix this?
Update
itemsData: [
{
code: "Test",
}
]
You should populate itemsData in computed method using ...mapState
Parent.vue
export default {
data: function () {
return {
items: this.itemsData
}
},
computed:{
...mapState('module/namespace', ['itemsData'])
}
}
<div class="col-sm-2" v-for="(item,index) in items" :key="index">
<ItemWidget :item="item" />
</div>
There is another way to declare your props:
<template>
<div class="label">
<div class="label-value">{{ item.code }}</div>
</div>
</template>
<script>
export default {
props: {
type: Object,
default: null
}
};
</script>

Error in render function: "TypeError: Cannot read property of undefined" in Vue

I am using Laravel and vue-router.
<template>
<div class="content__inner">
<div class="forums">
<!-- Heading -->
<div class="forums__heading" :style="'border-bottom:2px solid #' + board.category.color">
<div class="lg-8 md-8 sm-12 column column__first">
<h2 class="forums__heading__title">{{ board.title }}</h2>
</div>
<div class="lg-1 md-1 sm-1 dtop column text-center">
<strong>Replies</strong>
</div>
<div class="lg-3 md-3 sm-4 column text-right">
<strong>Latest Reply</strong>
</div>
<div class="clearfix"></div>
</div>
<!-- Content -->
<div class="forums__content">
{{ board.category }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
board: [],
}
},
created() {
this.fetch_board(this.$route.params.slug);
},
methods: {
/**
* Fetch the board.
*
* #param string slug The slug for the board.
*/
fetch_board(slug)
{
this.$http.get('/api/forums/board/' + slug).then((response) => {
this.board = response.data;
});
},
}
};
</script>
The 'fetch_board' function returns an object like the following:
board:Object {
id:5,
title:"Game Discussion",
slug:"5-game-discussion",
description:"General talk about the game.",
restriction:null,
category_id:2,
category:Object {
id:2
title:"Community",
color:"2ECC71",
created_at:"2017-05-02 07:30:25",
updated_at:"2017-05-02 07:30:25",
}
created_at:"2017-05-02 07:30:25",
updated_at:"2017-05-02 07:30:25",
}
When I access the {{ board.category }} it displays the object correctly; but when I access {{ board.category.title }} it displays the title, but ALSO gives a TypeError.
Why I am getting this error if the data is being loaded correctly?
How can I avoid/fix this error?
You are seeing this error because you are initializing "board" to an empty array. The component tries to evaluate "board.category.title" when it binds the reactivity just prior to the created() hook.
With board set as an empty array, step by step the evaluation might look like this:
const board = [];
const category = board.category; // undefined
const title = category.title; // TypeError, because category is undefined
You should stop seeing this error if you initialize your data like so:
data() {
return {
board: {
category: {
title: ''
}
}
}
}
Here is the Vue lifecycle diagram which illustrates when the created() event is fired
This error is explained in the official Vue documentation:
Since Vue doesn’t allow dynamically adding root-level reactive properties, you have to initialize Vue instances by declaring all root-level reactive data properties upfront, even with an empty value:
var vm = new Vue({
data: {
// declare message with an empty value
message: ''
},
template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'
If you don’t declare message in the data option, Vue will warn you that the render function is trying to access a property that doesn’t exist.

VueJS 2: Catch event of direct child component

I'm currently trying to get a simple Tabs/Tab component up and running.
It seems like something in the event handling mechanism has changed, therefore I can't get it to work.
Current implementation:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<li class="tab" v-for="tab in tabs" #click="activateTab(tab)">{{ tab.header }}</li>
</ul>
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: [],
data() {
return {
tabs: []
}
},
created() {
this.$on('tabcreated', this.registerTab)
},
methods: {
registerTab(tab) {
this.tabs.push(tab);
},
activateTab(tab) {
}
}
}
</script>
Tab.vue
<template>
<div class="tab-pane" v-show="active">
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
mounted() {
this.$emit('tabcreated', this);
}
}
</script>
eventhub.js
import Vue from 'vue';
export default new Vue();
View
<tabs>
<tab header="Test">
First Tab
</tab>
<tab header="Test2">
Second Tab
</tab>
<tab header="Test3">
Third Tab
</tab>
</tabs>
I've tried the following things:
use a Timeout for the $emit to test if it's a timing issue (it is
not)
use #tabcreated in the root element of the Tabs components
template
It works if...
... I use the suggested "eventhub" feature (replacing this.$on and
this.$emit with hub.$on and hub.$emit)
but this is not suitable for me, as I want to use the Tabs component multiple times on the same page, and doing it with the "eventhub" feature wouldn't allow that.
... I use this.$parent.$emit
but this just feels weird and wrong.
The documentation states that it IS possible to listen for events triggered by $emit on direct child components
https://v2.vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced
Does anyone have an Idea?
You're right, in vue 2, there is no more $dispatch. $emit could work for a single component but it will be scoped to himself (this). The recommended solution is to use a global event manager, the eventhub.
the eventhub can be stored in the window object to be used anywhere without import, I like to declare in my main.js file like this:
window.bus = new Vue()
and then in whatever component:
bus.$emit(...)
bus.$on(...)
It works just the same as this.$root.$emit / this.$root.$on. You said it works when you call this.$parent.$emit, but this code, simulate a scoped emit in the parent component but fired from the child, not good.
What I understand in your code is that you want to have an array of created tabs, but to do what with them ?
Instead of storing the tab instance in the parent and then activate from the parent, you should think about a more functional way.
The activateTab method should be declared on the tab component and manage the instanciation through the data, something like:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tab in tabs" :header="tab.header"></tab>
</ul>
</div>
</template>
<script>
import hub from '../eventhub';
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: ['First Tab', 'Second Tab', 'Third Tab']
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ header }}</span>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This way, your Tab is more independant. For parent/child communication keep this in mind :
parent to child > via props
child to parent > via $emit (global bus)
If you need a more complexe state management you definitely should take a look at vuex.
Edit
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tabData in tabs" :custom="tabData"></tab>
</ul>
</div>
</template>
<script>
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: [
{foo: "foo 1"},
{foo: "foo 2"}
{foo: "foo 3"}
]
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ custom.foo }}</span>
</div>
</template>
<script>
export default {
props: ['custom'],
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This is what I don't like about VueJS (2), there is no convenient way of catching events emitted from child components to the parent component.
Anyways an alternative to this is if you do not want to use the eventhub approach, specially if you are only going to have an event communication between related components ( child and parent ) and not with non-related components, then you can do these steps.
reference your parent vue component on its data property (very important, you can't just pass this to the child component)
pass that parent vue component reference as an attribute to the child component ( make sure to bind it)
trigger the appropriate event of the parent component inside the child component whenever a desired event is emitted
Pseudo code
// Parent vue component
Vue.component( 'parent_component' , {
// various codes here ...
data : {
parent_component_ref : this // reference to the parent component
},
methods : {
custom_event_cb : function() {
// custom method to execute when child component emits 'custom_event'
}
}
// various codes here ...
} );
// Parent component template
<div id="parent_component">
<child_component :parent_component_ref="parent_component_ref"></child_component>
</div>
// Child component
Vue.component( 'child_component' , {
// various codes here ...
props : [ 'parent_component_ref' ],
mounted : function() {
this.$on( 'custom_event' , this.parent_component_ref.custom_event_cb );
this.$emit( 'custom_event' );
},
// You can also, of course, emit the event on events inside the child component, ex. button click, etc..
} );
Hope this helps anyone.
Use v-on="$listeners", which is available since Vue v2.4.0. You can then subscribe to any event you want on the parent, see fiddle.
Credit to BogdanL from Vue Support # Discord.

Categories