All about the V-Model directive in Vue JS

Sushmita Tardia
8 min readNov 28, 2020

--

If you are a Vue Js developer like me, you would have or will surely encounter the very interesting “v-model” directive. Let’s dive into a lot of details about it.

V-model directive is a super helpful and a very clean approach for both binding any changes to a value and also listening for its changes.

Its most simple implementation is as follows:

An input box whose value is stored in the variable inputValue and it listens to changes using @input

<template>
<input :value="inputValue" @input="inputValue =
$event.target.value"/>
</template>

can be written using v-model as :

<template>
<input v-model="inputValue"/>
</template>

Our “inputValue” is a reactive property and hence should be present either as a data property or a computed property.

Using as a data property we can watch its changes using Vue watcher

data() {
return {
inputValue: ""
}
},

watch: {
inputValue(newValue, oldValue) {
console.log(newValue, oldValue)
}
}

Both its initialisation and watching on updates can also be achieved using accessor properties get() and set() in computed hook. Accessor properties are nothing but functions which are called when getting and setting a value.

computed: {
inputValue: {
get() {
return this.updatedInputValue;
},

set(newValue) {
this.updatedInputValue = newValue;
}
}
}

However using the set accessor properties, we will not be able to get the oldValue as compared to when using watcher.

We have seen how v-model works on a simple input, now lets see its more complex usages.

Radio Buttons

Radio buttons provide options to select one among many, hence the v-model will store just one primitive value which is the selected one.

<template>
<div>
<input type="radio" v-model="selectedRadioValue" id="radio1"
value="Value1">
<label for="radio1">Car</label>

<input type="radio" v-model="selectedRadioValue" id="radio2"
value="Value2">
<label for="radio2">Bike</label>
</div>
</template>

selectedRadioValue has to be a reactive value and depending on the selection will store Value1 or Value2.

Note: It is very important to give the value attribute for every input element here. This attribute is the one that gets assigned to our selectedRadioValue property. If we do not give the value attribute then it is assigned as null for every input element and then on any radio selection, all the radio buttons will get selected with the value null :(

CheckBox

Checkboxes can be either single-select or multi-select and accordingly v-model would be

  • A boolean value for single-select, indicating whether the checkbox is selected or not. In the example shown below, “checked” would be a data property storing either true or false.
<input type=”checkbox” id=”checkbox1” v-model=”checked”>
<label for="checkbox1">Checked</label>
  • Array of values indicating the values selected for multi-select.
    Here checked is initialised to an empty array([]) and gets filled with the values selected.
<input type=”checkbox” id=”checkbox1" v-model=”checked”  
value=”Checked 1">
<label for=”checkbox1">Audi</label>
<input type=”checkbox” id=”checkbox2" v-model=”checked”
value=”Checked 2">
<label for=”checkbox2">Jaguar</label>
<input type=”checkbox” id=”checkbox3" v-model=”checked”
value=”Checked 3">
<label for=”checkbox3">Porsche</label>

Note: It is important to give the value attribute here as well, in case we do not give, null would be assigned as the value to it and on any checkbox selection, all the checkboxes would get selected with the value as null.

Select

Select can again be a single-select or multi-select and correspondingly its v-model can be

  • A primitive value in case of the single select. Here selected would be a primitive value and depending on the selection, Audi/Jaguar/Porsche would get assigned to the “selected”.
<select v-model=”selected”>
<option selected>Audi</option>
<option>Jaguar</option>
<option>Porsche</option>
</select>
  • An array in case of multi-select. Here selected would be an array and on any selection, the selected values will get pushed to the selected array.
<select v-model=”selected” multiple>
<option selected>Audi</option>
<option>Jaguar</option>
<option>Porsche</option>
</select>

Note: As you would have already noticed, value attribute in the option element is not mandatory here as compared to that for Radio and Checkbox. If the value attribute is not present, option element’s innerText value gets assigned to our “selected” property. But if present, it will override the inner-text value and gets assigned to the “selected”.

Radio, Checkbox and Select emits “change” event on selection which gets captured by the v-model.

Between Components

v-model can also be used between components of parent and child relationship. It is used as a shorthand for

  1. passing a prop as “value” from parent to child and
  2. also listening to any changes to it through the input event emitted from the child to the parent.

For example, we have a parent component: App.vue

<template>
<div id="app">
Enter user count : <input v-model.number="userCount" />

<div v-for="(user, index) in userCount" :key="user">
<user-details v-model="allUsersModel[index]" />
</div>
<div>allUsersModel: {{$data.allUserModel}}</div>
</div>
</template>

<script>
import userDetails from "./components/userDetails";

export default {
name: "App",
components: {
userDetails,
},
data() {
return {
userCount: 0,
allUsersModel: [],
};
}
};
</script>

Child component: UserDetails.vue

<template>
<div>Name: <input v-model="nameModel" /></div>
</template>

<script>
export default {
name: "userDetails",
props: {
value: {
required: true
},
},
watch: {
nameModel(newValue, oldValue) {
console.log(newValue, oldValue);
this.$emit("input", newValue);
},
},
data() {
return {
nameModel: this.value
};
}
};
</script>

Few important points to note here:

  1. We should never try to mutate the prop directly ie, we should not use
    <input v-model=”value” /> in UserDetails.vue
    Vue will throw a warning:

[Vue warn]: 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: “value”

Interestingly, this warning is not thrown when we use “:value” and “@input”

<input :value="value" @input="$emit('input', $event.target.value)"/>

2. In App.vue, we have used v-model.number, here number is a modifier, more on it towards the end.

3. On every individual user-details component, we are using
v-model=”allUsersModel[index]”.
allUsersModel
is an array which stores and binds to the value emitted with the input event using this.$emit(“input”, newValue) from UserDetails.vue

Deeply Nested Structures

Now if we want to show a few more fields in UserDetails like age, we can create a deep nested structure for it, in allUsersModel and use it as the
v-model.

Parent component: App.vue

<template>
<div id="app">
Enter user count : <input v-model.number="userCount" />
<div v-for="(user, index) in userCount" :key="user">
<user-details v-model="allUsersModel[index].userModel" />
</div>
<div>allUsersModel: {{$data.allUsersModel}}</div>
</div>
</template>

<script>
import userDetails from "./components/userDetails";

export default {
name: "App",
data() {
return {
userCount: 0,
allUsersModel: [],
};
},
components: {
userDetails,
},
watch: {
userCount(newValue, oldValue) {
if(newValue) {
this.allUsersModel = [];
for(var i = 0; i < newValue; i++ ) {
this.allUsersModel.push({
userModel: {
name: undefined,
age: undefined
}
})

}
}
},

allUsersModel: {
handler: function(newValue) {
console.log("allUsersModel: ",newValue);
},
deep: true
},
}
};
</script>

Child component: UserDetails.vue

<template>
<div>
<span>
Name: <input v-model="nameModel" />
</span>
<span>
Age: <input v-model="ageModel" />
</span>
</div>
</template>

<script>
export default {
name: "userDetails",
props: {
value: {
required: true,
},
},
watch: {
localModel(newValue, oldValue) {
console.log(newValue, oldValue);
this.$emit("input", newValue);
},
},
data() {
return {
nameModel: this.value.name,
ageModel: this.value.age
};
},
computed: {
localModel() {
return {
name: this.nameModel,
age: this.ageModel
}
}

}
};
</script>

Important points to note here are:

  1. In the parent component, we are using allUsersModel[index].userModel as the v-model. userModel property on each index will store and bind and also listen to its changes through the input event from the child component.
  2. Since allUsersModel is now a deeply nested array of objects, we would have to use “deep: true” in its watcher, otherwise the watcher will not get called.
  3. In the child component, we have created data properties nameModel and ageModel, as we cannot mutate the “value prop”. We use them as the v-models for the child component.
  4. We also have created a computed property called “localModel”, which gets updated every time the value prop from parent changes or the nameModel or ageModel from the child changes. Now this comes as handy to just watch over it and emit this with the input event.

This approach can now be extended to any number of levels in component hierarchy ie parent -> child -> grand-child -> …..
by receiving value prop in the child from the parent and emitting an input event to the parent.

Watching deep nested object/arrays

As we know when updating the value and reference of a child of an array, the reference of the outer array will remain the same, unless we do JSON.parse(JSON.stringify(array)) ie. creating a deep copy of it.

On the same lines with any name/age update, Vue will update the value and reference of the userModel (child of allUsersModel), but the reference of allUsersModel will still remain the same.

In this case, if we watch on allUsersModel, we will not get the oldValue, as the newValue’s and the old Value’s references will be the same and Vue doesn't keep track of the old value.

In such cases, if we very much require the oldValue, let’s say to compare oldValue and newValue and take action only when they are not the same, v-model will not be of help here.
Here, we would have to use “:value” and “/@input”

<user-details :value="allUsersModel[index].userModel" @input="val => onModelUpdate(val)" />

Customizing v-model on a component

In case we do not want to use the default value prop and the input event on the v-model, we can customize it using the “model” option in the child component.

In the model option, we provide a new prop and event properties to be used for the v-model from the parent.

For example, for select, we can send a change event as follows:

Parent component: App.vue

<template>
<vacation v-model="location" />
</template>

<script>
import vacation from "./components/vacation";

export default {
name: "App",
components: {
vacation,
},
data() {
return {
location: undefined,
};
},
watch: {
location(newValue, oldValue) {
console.log(newValue, oldValue);
},
},
};
</script>

Child component: Vacation.vue

<template>
<div>
Select next vacation
<select :value="value" @change="$emit('change',
$event.target.value)">

<option>Hills</option>
<option>Lakes</option>
<option>Beaches</option>
</select>
</div>
</template>

<script>
export default {
name: "HelloWorld",
model: {
prop: "value",
event: "change",
},
props: {
value: String,
},

};
</script>

Modifiers

  1. .number
    By default, the input value would be a string. In the case we want the input to be stored as a number, instead of using parseInt in JS, we can directly use v-model.number which would return the input value in number.
  2. .trim
    This is again a helpful modifier to trim the input value of any white-spaces.
  3. .lazy
    By default, with every input event on the input element, the sync happens with its value. We can change it such that the sync happens with the change event using the .lazy modifier.

CONCLUSION

We have discussed about the v-model directive and the many different ways to use it whether on simple input/select to more complicated components. We can see they are very easy to learn and use and also helps us in writing a very clean code.

I hope you found this article helpful. Please do hit the 👏 button to show your appreciation and feel free to comment below!

--

--

Sushmita Tardia

Software Engineer, Web Developer, Traveller, Nature Lover