The v-model directive enables two-way data binding between a parent and a child component. This means that the changes made in the parent component are immediately reflected in the child component. Similarly, when the state changes in the child component, it's mirrored in the parent component. In other words, v-model it allows us to sync up the states in two components. This is very useful with form elements, such as inputs. So, let's see how to effectively use v-model with custom components.
Basic input
In our starter app, we have a root App.vue component that renders a child component, BaseInput.vue. The root component has a firstName variable and our goal is to pass this variable to the child component. Also, if any changes happen to this variable in the child component, we want to catch them and send to the parent component. Here's the first step for accomplishing this:
<script setup>
import { ref } from 'vue';
import BaseInput from './components/BaseInput.vue';
const firstName = ref('Lera');
</script>
<template>
<BaseInput v-model="firstName"/>
{{ firstName }}
</template>
We passed down the firstName variable as a prop into the child component using the v-model. The firstName also appears below the BaseInput component — we'll need it in the future for testing purposes.
Now, the child component must accept this variable and do the rest of the logic. To make that variable visible and accessible in the child component, we should use defineProps() macro and specify the modelValue attribute. This is the default name, we'll see how to customize it later. Here's what the BaseInput.vue component looks like:
<script setup>
defineProps({
modelValue: String
});
</script>
<template>
<input :value="modelValue" />
</template>
There are a couple of ways to define props. One of them is using the defineProps() macro. It can accept an array or object with the prop names. In this example, we went with the object that has the modelValue as a prop and String as a type for this prop. Inside the template, you can see that we bound that prop to the input via the v-bind directive.
props variable and assign the defineProps() to it. Otherwise, Vue will throw an error saying that the prop is not defined. Open your browser and check if the name appears inside the input field:
Cool! This means that we were able to pass the data down to the child component. However, if you type something into the field, the actual firstName variable doesn't change. This is because we didn't provide any event that will actually modify the state that came from the parent component.
Our final step would be to provide the input field with the input event that registers when a user types something and then emits another event that modifies the state. Here's the code:
<script setup>
defineProps({
modelValue: String
});
const emit = defineEmits(['update:modelValue']);
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
First, we define a new constant emit and equal it to defineEmits() macro. The macro takes in an array with all emit names in the component. In the template, the emit event takes in the event name as the first argument and the value as the second argument. event.target.value is the current letter that a user types in, and we're basically updating our modelValue with each keystroke.
Take a look at the result: whatever we type in is reflected in the firstName variable:
Custom names
In Vue.js, changing the default modelValue name to a different one is also possible. It's useful when you have a lot of components, and you don't want to get confused. Custom names provide better readability, and it's easier to figure out what each input does if it has a meaningful name.
To change the default name, simply add a column and write the name you want. Here's how we can change our previous example:
<BaseInput v-model:first-name="firstName" />
Now that we've specified a new name, we also need to include this name in the child component. Head over to the BaseInput.vue file and change modelValue to firstName everywhere:
<script setup>
defineProps({
firstName: String
});
const emit = defineEmits(['update:firstName']);
</script>
<template>
<input
:value="firstName"
@input="emit('update:firstName', $event.target.value)"
/>
</template>
The app should work the same.
defineProps() macro the prop names must always be camel-cased. When passing the props onto the components, they could either be camelased or kebab-cased. It's important to preserve one style across the app. Multiple v-models
Imagine we have a reusable component with a few inputs in it. It's a sort of widget, and we can insert it anywhere in our application. It has a field for name, last name, and city. We want to pass in three v-models and capture input events for each of them. Let's start implementing this! In the App.vue file, add two more variables for last name and city. Then, pass the props in, giving each of them a unique name. This is where custom names that we learned about previously come in handy.
<script setup>
import { ref } from 'vue';
import UserCard from './components/UserCard.vue';
const firstName = ref('');
const lastName = ref('');
const city = ref('');
</script>
<template>
<h2>{{ firstName }} {{ lastName }} {{ city }}</h2>
<UserCard
v-model:first-name="firstName"
v-model:last-name="lastName"
v-model:city="city"
/>
</template>
Time to work on the UserCard component. You can create it and put it inside the components folder if you haven't yet. It'll now accept three props; all are of the String type; each of the inputs will have its own bound property, which is also reflected in the input event:
<script setup>
defineProps({
firstName: String,
lastName: String,
city: String
});
const emit = defineEmits(['update:firstName', 'update:lastName', 'update:city']);
</script>
<template>
<input
:value="firstName"
placeholder="name"
@input="emit('update:firstName', $event.target.value)"
/>
<input
:value="lastName"
placeholder="last name"
@input="emit('update:lastName', $event.target.value)"
/>
<input
:value="city"
placeholder="city"
@input="emit('update:city', $event.target.value)"
/>
</template>
Here's the output: whenever you type something, it appears on the top:
That's it for our small app. Hopefully, you now have a better understanding of how v-model works with custom components. You may experiment with other input fields, such as checkbox, select, textarea, and so on. Most of them work similarly: you just need to check the correct event name and apply some changes to the event handler. As an example, I suggest we take a closer look at checkboxes in the next section since they are used in many apps.
Custom checkbox component
Checkboxes hold a true/false value, so in this example, we'll work with Boolean type and think of a way to toggle between true and false. To start, initialize an answer constant and set it to false. As usual, pass it down to the child component via a v-model like this:
<script setup>
import { ref } from 'vue';
import Checkbox from './components/Checkbox.vue';
const answer = ref(false);
</script>
<template>
<h2>Is v-model great? {{ answer }}</h2>
<Checkbox v-model:answer="answer" />
</template>
In the child component (Checkbox.vue) define the answer prop and set it to Boolean. This time, we'll have to listen for the change event. As a payload to the change event, we need to send in a boolean value. The answer is either true or false; if it's currently true, we need to change it to false, and vice versa. We can do this via a ternary operator. Take a look at the complete version of our child component:
<script setup>
defineProps({
answer: Boolean
});
const emit = defineEmits(['update:answer']);
</script>
<template>
<input
:value="answer"
type="checkbox"
@change="emit('update:answer', answer ? false : true)" />
</template>
Output:
As you can see, working with checkboxes is easy too. We only needed to use another event listener and modify the event's payload.
Conclusion
To sum up, v-model with custom components enables two-way data binding between components and their parent. By defining a prop and emitting an event, we can create custom components that can be easily reused across multiple projects. Using v-model with custom components simplifies the process of creating complex user interfaces and reduces the amount of code needed to manage data.