I have two components with parent-child relations. The parent component can contain any number of child components using a v-for loop.
Essentially the child component contains a bootstrap table b-table and the parent component is just a wrapper around the child component. Therefore, the parent component can have any number of tables inside it.
The tables have the functionality to check or uncheck rows of the table. I want to keep track of all the selected rows of all the tables inside the parent component. I am using a Vuex store to keep the id of all the selected rows from all the tables. Each child component commits to the vuex store after any rows is checked/unchecked. It all works all fine till here. Using the vue devtools I can confirm that the vuex store always has the correct data in it.
(the vuex state property name is selectedObjects, it is an array of strings).
Now, I want to display some data on the parent component based of the vuex store value (using v-show). I want to show the data only when selectedObjects in the store is not an empty array.
I have a getter to get the selectedObjects state from the store, and I use this getter in the parent component as a computed property using mapGetters.
The problem is that everytime a child component makes any change to the vuex store, the parent component refreshes the prop that is passed on to the child components. The prop being passed is not dependant on the vuex store at all.
<template>
<div>
<div v-if="isDataLoaded">
<div>
<div>
<div>
<span v-show="showBulkActions"> Action1 </span>
<span v-show="showBulkActions"> Action2 </span>
<span v-show="showBulkActions"> Action3 </span>
<span v-show="showBulkActions">Action4</span>
<span> Action5 </span>
</div>
</div>
</div>
<div>
<template v-for="objectId in objectIds">
<ObjectList
:key="objectId"
:objects="getObjectsFor(objectId)"
:payload="payload"
></ObjectList>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
payload: Object,
isDataLoaded: false
},
data() {
return {
objectIds: [],
objects: []
};
},
computed: {
...mapGetters(["getSelectedObjects"]),
showBulkActions() {
// return true; --> works fine
//This does not work as expected
const selectedObjects = this.getSelectedObjects;
return selectedObjects.length > 0;
}
},
mounted: async function() {
// init this.objects here
},
methods: {
...mapGetters(["getObjects"]),
getObjectsFor(objectId) {
//this line gets executed everytime the selectedObjects is modified in vuex store.
//this method is only being called from one place (prop of ObjectList component)
return this.objects.filter(f => f.objectId === objectId);
}
}
};
</script>
According to my understanding, the getObjectsFor method should not be called when the selectedObjects array in vuex store is changed, because it does not depend on it.
Why is this happening ?
Your parent component template depends on selectedObjects Vuex store value (through showBulkActions computed prop and getSelectedObjects getter as computed prop).
Every time selectedObjects changes in store, update (and re-render) of parent component is triggered.
And as stated in Vue documentation:
every time the parent component is updated, all props in the child component will be refreshed with the latest value
This means expressions used to populate child components props (call to getObjectsFor method in your case) needs to be evaluated. That's why your method is called.
Solution would be to pass all objects to your child components as a prop and do the filtering (done in getObjectsFor method) inside your child component as computed prop...
Related
I have 3 vue.js nested components: main, parent, child.
The parent component load basic data, the child is a simple countdown widget which needs just a data to be configured.
If I set the parent script with static data (IE deadline='2019-12-12') the child show the widget working nice, but if I use dynamic data it generate error.
I'm using computed to pass data to the child component and if I debug it using an alert I see 2 alerts: undefined and then the correct date.
The issue is the first computed data (undefined) crash the widget, so how to create child component using updated (loaded) data?
Parent template:
<template>
<div>
<flip-countdown :deadline=deadline></flip-countdown>
</div>
</template>
Parent Script: it needs to be fixed
export default {
components: {FlipCountdown},
props: ['event'],
computed: {
deadline: function () {
if (typeof(this.event.date)!="undefined") {
//alert(this.event.date)
return this.event.date;
} else {
return "2019-05-21 00:00:00";
}
},
},
Child template: it works
<template>
<div>
<flip-countdown :deadline="deadline"></flip-countdown>
</div>
</template>
Your parent component passes the deadline to its child component before the mounted lifecycle hook fires. Your child component sets its deadline with the initial value of undefined.
You should make deadline a computed property in child component:
computed: {
internalDeadline() {
return this.deadline; // comming from props
}
}
Then you can use internalDeadline in child.
Alternatively, you could wait to render the child component until deadline is defined:
<flip-countdown v-if="deadline !== undefined" :deadline="deadline"></flip-countdown>
Component Code
<template lang="html">
<div class="chat-users">
<ul class="list-unstyled">
<li v-for="chatuser in chatusers" class="left clearfix">
{{ chatuser.UserName }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["chatusers"],
created() {
axios.post("some url").then(response => {
this.chatusers = response.data.Data;
});
}
}
</script>
<style>
</style>
Everything works perfectly, but I am getting below warning.
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:
"chatusers"
There is an explanation why prop should not be mutate in the vue documentation. If you need to mutate the state, perhaps you need a data instead.
Like:
export default {
data () { return { chatusers: null } },
created() {
axios.post("some url").then(response => {
this.chatusers = response.data.Data;
});
}
}
It is not the best way to mutate the props, since the parent is in control of this data and any changes it will overwrite child data, from the docs:
In addition, every time the parent component is updated, all props in
the child component will be refreshed with the latest value. This
means you should not attempt to mutate a prop inside a child
component. If you do, Vue will warn you in the console.
According to Vue.js data flow, the props received from parent should not be modified by child component. See the official documentation and this Stack Overflow answer.
As suggested by your warning: "use a data or computed property based on the prop's value", that in your case is the response received from axios call.
This is an easy question but I dont know the best way to do it correctly with Vue2:
Parent Component:
<template>
<div class="parent">
<child></child>
{{value}} //How to get it
</div>
</template>
Child Component:
<template>
<div class="child">
<p>This {{value}} is 123</p>
</div>
</template>
<script>
export default {
data: function () {
return { value: 123 }
}
}
</script>
Some ways you can achieve this include:
You can access the child VM directly via a ref, but this gets hairy because the ref won't be populated until the child component has mounted, and $refs isn't reactive, meaning you'd have to do something hacky like this:
<template>
<div class="parent">
<child ref="child"></child>
<template v-if="mounted">{{ $refs.child.value }}</template>
</div>
</template>
data() {
return {
mounted: false
}
},
mounted() {
this.mounted = true;
}
$emit the value from within the child component so that the parent can obtain the value when it is ready or whenever it changes.
Use Vuex (or similar principles) to share state across components.
Redesign the components so that the data you want is owned by the parent component and is passed down to the child via a prop binding.
(Advanced) Use portal-vue plugin when you want to have fragments of a component's template to exist elsewhere in the DOM.
Child components pass data back up the hierarchy by emitting events.
For example, in the parent, listen for an event and add the child value to a local one (an array for multiple child elements would be prudent), eg
<child #ready="childVal => { value.push(childVal) }"></child>
and in the child component, $emit the event on creation / mounting, eg
mounted () {
this.$emit('ready', this.value)
}
Demo ~ https://jsfiddle.net/p2jojsrn/
I have a parent component that contains an array and an object:
data() {
return {
products: [],
attributes: {},
}
},
When my parent component is loaded, it gets some AJAX data and populates the variables:
mounted() {
axios.get('/requests/getProducts/' + this.currentCategory).then(response => {
this.products = response.data;
this.createAttributes(); // runs some methods to populate the attributes object
});
},
I then have a child component that I will use to display the attributes. Code from parent template:
<search-attributes :attributes="attributes"></search-attributes>
I have the props declared in my child component:
props: ['attributes'],
So far so good, I can console log the data from parent and child...
Problem is when I try to v-for the attributes in the child component, it simply returns nothing:
/////////////// this does not render anything ///////////////
<template>
<div>
<div v-for="(value, key) in attributes">
{{ key }}
</div>
</div>
</template>
However, if I pass both the products and attributes variables to the child component, and render products in the child component, attributes will start working!
/////////////// this works /////////////
<template>
<div>
<div v-for="(value, key) in attributes">
{{ key }}
</div>
{{ products }}
</div>
</template>
What the heck is going on?
Do you need to see my parent methods?
I expect you are running into a change detection caveat. Vue cannot detect when you add properties dynamically to an object. In this case, you begin with an empty attributes object. If you add properties to it without using $set, then Vue does not understand a change has occurred and will not update the DOM.
Update the createAttributes to use $set.
The reason it works when you pass products is Vue can detect the change you make (this.products = response.data), so it renders the DOM, which shows the latest attributes as a by product.
I have a very small app that has a donation form. The form walks the user through the steps of filling in information. I have a main component, which is the form wrapper and the main Vue instance which holds all of the form data (model). All of the child components are steps within the donation process. Each child component has input fields that are to be filled out and those field will update the parent model so that I have all of the form data in the parent model when I submit the form. Here is how the components are put together:
<donation-form></donation-form> // Main/Parent component
Inside the donation-form component:
<template>
<form action="/" id="give">
<div id="inner-form-wrapper" :class="sliderClass">
<step1></step1>
<step2></step2>
<step3></step3>
</div>
<nav-buttons></nav-buttons>
</form>
</template>
Right now, I am setting the data from the inputs in each child component and then I have a watch method that is watching for fields to update and then I am pushing them to the $root by doing this...
watch: {
amount() {
this.$root.donation.amount = this.amount;
}
}
The problem is that one of my steps I have a lot of fields and I seem to be writing some repetitive code. Also, I'm sure this is not the best way to do this.
I tried passing the data as a prop to my child components but it seems that I cannot change the props in my child component.
What would be a better way to update the root instance, or even a parent instance besides add a watch to every value in my child components?
More examples
Here is my step2.vue file - step2 vue file
Here is my donation-form.vue file - donation-form vue file
You can use custom events to send the data back.
To work with custom events, your data should be in the parent component, and pass down to children as props:
<step1 :someValue="value" />
and now you want to receive updated data from child, so add an event to it:
<step1 :someValue="value" #update="onStep1Update" />
your child components will emit the event and pass data as arguments:
this.$emit('update', newData)
the parent component:
methods: {
onStep1Update (newData) {
this.value = newData
}
}
Here is a simple example with custom events:
http://codepen.io/CodinCat/pen/QdKKBa?editors=1010
And if all the step1, step2 and step3 contain tons of fields and data, you can just encapsulate these data in child components (if the parent component doesn't care about these row data).
So each child has its own data and bind with <input />
<input v-model="data1" />
<input v-model="data2" />
But the same, you will send the result data back via events.
const result = this.data1 * 10 + this.data2 * 5
this.$emit('update', result)
(again, if your application becomes more and more complex, vuex will be the solution.
Personally I prefer having a generic function for updating the parent, when working with forms, instead of writing a method for every child. To illustrate – a bit condensed – like this in the parent:
<template lang="pug">
child-component(:field="form.name" fieldname="name" #update="sync")
</template>
<script>
export default {
methods: {
sync: function(args) {
this.form[args.field] = args.value
}
}
}
</script>
And in the child component:
<template lang="pug">
input(#input="refresh($event.target.value)")
</template>
<script>
export default {
props: ['field', 'fieldname'],
methods: {
refresh: function(value) {
this.$emit('update', {'value': value, 'field': this.fieldname});
}
}
}
</script>
For your case you can use v-model like following:
<form action="/" id="give">
<div id="inner-form-wrapper" :class="sliderClass">
<step1 v-model="step1Var"></step1>
<step2 v-model="step2Var"></step2>
<step3 v-model="step3Var"></step3>
</div>
<nav-buttons></nav-buttons>
</form>
v-model is essentially syntax sugar for updating data on user input events.
<input v-model="something">
is just syntactic sugar for:
<input v-bind:value="something" v-on:input="something = $event.target.value">
You can pass a prop : value in the child components, and on change of input field call following which will change the step1Var variable.
this.$emit('input', opt)
You can have a look at this answer where you can see implementation of such component where a variable is passed thouugh v-model.