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.
Related
I have a Parent.vue, where i send an object to a Child component, and get back the updated item every now and then via this.$emit('updateItem', item).
<template>
<div>
<Child
v-for="(item, index) in items"
:key="index"
:item="item"
#updateItem="updateItem"
// #updateItem="updateItem(index, dataFromChildComponent)" // this is what i would like to do
/>
</div>
</template>
<script>
export default {
name: 'Parent',
data() {
return {
items: [] // List of objects
}
},
methods: {
updateItem (index, data) {
// Receive the data and the index
}
}
}
</script>
The problem i keep running into is how to locate the object in the list of items in the Parent to update the correct one. I would like to enrich this data to add e.g. an index to be able to locate the item in the list.
Is there any way to achieve this or is it actually the correct way to propagate the index down to the Child in order to emit it back up again? Feels like an anti-pattern to me.
You have several options to achieve that:
You can pass as a props index to child component and then in child emit function send object containing updated index with updated value.
You can change your current code to something like this: #updateItem="updateItem(index, $event)" where $event is updated value
Updating your event listener as #updateItem="data => updateItem(index, data)" should work. Assuming you have emitted data from child component (i.e. something like this.$emit('updateItem', somedata)).
I am currently stuck with a simple-looking issue but cannot find the wording.technicality behind solving it.
Essentially, I have a child component "project card" this card is rendered on my home page using
<div v-for="(project, index) in projects" :key="index">
(pulled from an array called "projects" which contain objects).
Anyway, my issue is, I have a button on the "project card" component, and clicking on that I would like to run a function on the "home" page. (as it seems to make sense to control the array from there).
But, how do I do that? How do i make a button pressed on a child component fire a function on the parent?
You need this in your child component:
this.$emit('myEvent')
Then you can do on your parent component:
<my-parent-component v-on:myEvent="doSomething"></my-parent-component>
You can read about firing events at https://v2.vuejs.org/v2/guide/components-custom-events.html . If this is the simple case then using event emiting is fine. But in another cases the better decision is using vuex.
emit the event and register it in the child component using the "emits" property. Listen to the emitted event at the parent element where you have created the child instance and point the event listener to the function you want to execute.
For Eg.
Parent
<template>
<child #close="closeIt"></child>
</template>
<script>
export default {
methods: {
closeIt() {
//DO SOMETHING...
}
}
}
</script>
Child
<template>
<button #click="closeInParent">Close</button>
</template>
<script>
export default {
emits: ["close"],
methods: {
closeInParent() {
this.$emit('close')
}
}
}
</script>
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...
I would like to have two components, one for displaying a value, and one for changing it with a text field. I can't get this to work? Is there another way of doing this?
I get this error message:
"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: "forpris""
Vue.component('prislapp-forpris', {
props: ['forpris'],
template: '<div class="prislappForpris">[[ forpris ]],-</div>',
delimiters: ['[[',']]']
});
Vue.component('input-forpris', {
props: ['forpris'],
template: '<input type="text" v-model="forpris" />'
});
var app = new Vue({
el: '.previewPage',
data: {
lapp: {
id: 1,
forpris: 30595
}
}
});
It's all about v-model directly mutating the forpris prop. As the warning states, you should avoid to mutate a prop from a component.
Rationale behind the warning
The reason is that allowing child component to modify props that belong to their parents make programs more error prone and difficult to reason about.
Instead, the idea behind Vue and other component oriented architectures and frameworks is that child components emit events to their parents, and then the parents change their own state, which in turn modify the child component via events from their children.
This ensures that the component passing down the props have full control of the state and may, or may not, allow the desired state changes that come via props.
How to fix your code to avoid the warning
v-model is syntax sugar over a :value and an #input on the input element. A really good read to understand how v-model innerly works is this article.
What you should do, is to drop v-model on the input for this:
template: '<input type="text" :value="forpris" #input="$emit('input', $event)" />'
This will set forpris as the value of the input (as v-model was already doing), but, instead of automatically modifying it, now the component will emit an input event when the user writes in the input.
So you now need to listen for this event in the parent and react accordingly. Now from your code is not absolutely clear who is rendering the two component, I guess the rendering comes from the .previewPage element in the Vue template, so the Vue instance is the parent component here.
You don't show the html of that template, but I guess it is something like the following:
<div class="previewPage">
<prislapp-forpris :forpriss="lapp.forpris" />
<input-forpris :forpriss="lapp.forpris" />
</div>
You now should listen to the #input event in the input-forpriss component:
<div class="previewPage">
<prislapp-forpris :forpriss="lapp.forpris" />
<input-forpris :forpriss="lapp.forpris" #input="handleInput" />
</div>
So, whenever we receive an #input event, we call the handleInput method. We also need to add such method to the Vue instance:
var app = new Vue({
el: '.previewPage',
data: {
lapp: {
id: 1,
forpris: 30595
}
},
methods: {
handleInput(value){
console.log(value); // now I'm not 100% sure if this
// is the value or a DOM event, better check
this.lapp.forpriss = value;
},
}
});
Evening. I've created a button which adds a component that has an input field inside. I might need to press that button few times so there would be 2-3 input fields that appear. Whenever I type the text I would like to send a request from the parent component but I don't know how to retrieve the data from every child component that has been created. Is this the time to start using vuex (never used it)?
ParentComponent.vue
<template>
<div>
<button class="btn btn-success" #click="addStep">Add step</button>
<div v-for="i in count">
<recipe-step v-bind:step-number="i"></recipe-step>
</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
addStep() {
this.count += 1;
}
}
}
</script>
StepComponent.vue
<template>
<div>
<div class="from-group">
<label for="step-input"></label>
<input id="step-input" v-model="text" type="text">
</div>
</div>
</template>
<script>
export default {
props: {
stepNumber: {
type: Number,
required: true
}
},
data() {
return {
step: this.stepNumber,
text: ""
}
}
}
</script>
No, you really don't need Vuex yet. As long as you are still dealing with parent-child-component communication, you should be fine. Vuex comes into play when components, spread across the hole component hierarchy, need to exchange information.
Now, you should do something like this:
Don't store the text in the child component. When the input changes, send a Custom Event right to the parent component. Note that
<input v-model="text">
is only syntax sugar for
<input :value="text" #input="text = $event">
Both have the same effect. That's way you can send the input event up to the parent, like this:
<input #input="$emit('input', $event)">
Add another prop to your child component called value which should replace text.
And now you can use v-model in the parent component:
<recipe-step v-model="text">
To store multiple values, just use an array in your data properties.
<recipe-step v-model="textArray[i]">
Vuex can help you on that, however if all you want is to get the input text value back to the parent with the minimum effort you can create a prop called value in the children and then pass it as v-model in the parent.
Since you have a v-for you could make it iterate over a list instead a counter and then pass some prop inside each item as v-model