Using Vue's v-model With Objects
I'll occasionally run across a scenario where I have an object or a list of objects that would be useful to extract into a child component. You could go through the process of creating HTML elements for each item in the object and directly binding a v-model to the input elements but this is Vue! We have the power to extract these repeated fields into a child component and save ourselves from having to generate tons of HTML boilerplate.
JavaScript objects are pass by reference which through this quirk means that we could effectively bypass Vue's parent-child events by altering the object directly. This is bad and can make troubleshooting your application very hard.
// Child component
// DO NOT DO THIS!
export default {
props: [value]
methods: {
update(key, newValue) {
value[key] = newValue
}
}
}
Example of using v-model with an Object
For this example let's say you have a list of known events that also have a description or some other metadata associated with them. These could be conferences, holidays or birthdays. We'll use an array of holidays for this example.
Data
An example of the type of data we are working with. A simple list of holidays which includes their name, the date and a description of the holiday.
data() {
return {
holidays: [
{
name: "Fourth Of July",
date: null,
description: ""
},
{
name: "Thanksgiving",
date: null,
description: ""
}
]
};
}
List the child components in the parent
In the parent we can loop the holiday array and bind the holiday to the child component using holiday[i]
in this case.
<div v-for="(item, i) in holidays" :key="i">
<h2 class="is-size-4">{{item.name}}</h2>
<Child v-model="holidays[i]"/>
</div>
The child
The child component should appear similar to most other components you've seen that use implement the v-model
pattern. The exception being we need to emit a copy of the entire object back to the parent. Remember, when passing down an object to the child we are passing down a reference. We must copy it before we alter the values on the object to avoid later headaches and follow the approved parent-child 1-one data flows.
Template
In the template the HTML input elements cannot use v-model because we need to control how the values are updated.
On the @input
event specify a method updateValue
and provide the first parameter as the name of the key on the object, the second parameter can be $event.target.value
or whatever you want to set the key's value to. Repeat this for all of the object's fields you wish to modify in the child component.
<template>
<div>
<div class="field">
<div class="control">
<label for="name" class="label">Name</label>
<input class="input" :value="value.name" @input="updateValue('name', $event.target.value)">
</div>
</div>
<div class="field">
<div class="control">
<label for="date" class="label">Date</label>
<input
class="input"
type="date"
:value="value.date"
@input="updateValue('date', $event.target.value)"
>
</div>
</div>
<div class="field">
<div class="control">
<label for="textarea" class="label">Textarea</label>
<textarea
class="textarea"
:value="value.description"
@input="updateValue('description', $event.target.value)"
/>
</div>
</div>
</div>
</template>
Script
For the script section, we need to accept a value prop in order to be compatible with v-model
.
Finally, the most important part is handling the emit event. We'll want to emit a standard input event but the value is going to be special.
The UpdateValue
method is going to emit the value of new object through this code { ...this.value, [key]: value }
. Here we will use 2 advanced JavaScript features that help keep our code very clean. The first is the object spread syntax which copies the object passed down via the prop and will merge the updated key/value into the object.
The second is using the computed property name which allows us to use the variable key
as the name of the key in the object. This allows us to have a generic updateValue
method that can accept a key and a value as parameters and using the inputs update the correct key on the object and emit it back to the parent.
export default {
props: {
value: {
type: Object,
required: true
}
},
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
}
}
};
Special Thanks to Dan Vega and his Gridsome Codesandbox Plugin