In this article, I will introduce changes to the v-model directive in Vue 3. Then, I will step through a tutorial, demonstrating how to use multiple v-model bindings to simplify the process of building complex forms in Vue.
What is the v-model directive?
The Vue v-model directive enables two-way data binding on form input elements, such as the input element, textarea element, and select element on any Vue component.
It handles data updates in two ways:
when the value of the input changes, the v-model reflects the value onto the state inside the component
when the state of the component changes, the v-model reflects the changes onto the form input elements
The v-model directive uses distinct properties and emits different events for different input elements by default:
value property and input event for text and textarea elements
checked property and change event for checkboxes and radio buttons
value as a prop and change event for select fields
A simple input element in a custom component will look something like this:
<input
type="text"
:value="modelValue"
@input="$emit(update:modelValue, $event.target.value)"
>
And its props will be defined like so:
props: {
modelValue: {
type: String,
default: '',
required: true
}
}
In the parent component, the custom component would be used like this:
<CustomComponent v-model:modelValue="name" />
// or the shorthand
<CustomComponent v-model="name" />
In the custom component, the v-model directive assumes an internal property has been defined with the name modelValue and emits a single event called update:modelValue.
You are not limited to the default naming convention; you may use a different name of your choosing. Having descriptive names for our v-model bindings enables us to use less code when trying to read and define what properties are attached to the parent component.
Just be sure to be consistent when selecting naming properties. Here’s an example of a custom name, fullName, used for the modelValue property:
<input
type="text"
:value="fullName"
@input="$emit(update:fullName, $event.target.value)"
>
props: {
fullName: {
type: String,
default: '',
required: true
}
}
<CustomComponent v-model:fullName="fullName" />
// or the shorthand
<CustomComponent v-model="fullName" />
How does v-model handle data binding?
The v-model directive has three modifiers that can be used for data binding: .lazy
, .number
, and .trim
. Let’s take a closer look.
.lazy
By default, v-model syncs with the state of the Vue instance after each input event is emitted. But with the .lazy modifier, the v-model allows the sync to occur after each change event.
Here’s an example showing the use of the .lazy modifier:
<input v-model.lazy="message" />
.number
The .number modifier, on the other hand, allows us to automatically convert a user entry to a number. An HTML input’s default value is always a string, so this modifier can be super helpful. If the value can’t be parsed into a number, the original value is returned.
Here’s an example showing the use of the .number modifier:
<input v-model.number="numPapayas" type="number" />
.trim
The .trim modifier, as the name suggests, automatically trims whitespace from the user input.
Here’s an example showing the use of the .trim modifier:
<input v-model.trim="message" />
How is the v-model in Vue.js 3 different from Vue.js 2?
If you’re familiar with using the v-model directive in Vue 2, you understand how complex it was with regard to creating forms.
In Vue 2 we were only allowed to use one v-model per component. To support two-way data binding in complex components, a full-blown payload had to be utilized on the v-model.
The component would handle the state of all the input elements and a single payload object would be generated to represent the state of the component. An event with the attached payload would then be emitted to the parent component.
This method created issues when it came to creating Vue UI libraries because it wasn’t always clear what was included in the payload. Developers had no choice but to loop through the payload object to ascertain what properties were included.
Fortunately, Vue 3 provides developers with more flexibility and power when it comes to building custom components that support two-way data binding. In Vue 3, we’re allowed as many v-model directives as we need. This can be quite convenient, as we’ll demonstrate later in this article.
Multiple v-model directive bindings tutorial
Let’s see how we can use multiple v-model directive bindings to simplify a complex Vue form.
For our example, we’ll use a checkout form that lists the user’s first name, last name, and email address, followed by some fields related to billing and delivery.
Creating the reusable component
The billing and delivery sections include the street name, street number, city, and postcode. But, since a user’s billing and delivery address are often the same, let’s create a reusable address component for the form.
First, we’ll set up the Vue app using the following command:
vue create <project-name>
Then, we’ll create a reusable component, AddressFieldGroup.vue
, inside a components folder within our src
folder.
This reusable component will be imported into our App.vue
file. With the v-model, this reusable component will be bound to a custom component in the App.vue
file.
Let’s take a closer look at the reusable component, AddressFieldGroup.vue
:
<template>
<section class="address">
<h2>{{ label }}</h2>
<div class="address__field">
<label for="streetName">Street name</label>
<input
type="text"
id="streetName"
:value="streetName"
@input="$emit('update:streetName', $event.target.value)"
required
/>
</div>
<div class="address__field">
<label for="streetNumber">Street number</label>
<input
type="text"
id="streetNumber"
:value="streetNumber"
@input="$emit('update:streetNumber', $event.target.value)"
required
/>
</div>
<div class="address__field">
<label for="city">City</label>
<input
type="text"
id="city"
:value="city"
@input="$emit('update:city', $event.target.value)"
required
/>
</div>
<div class="address__field">
<label for="postcode">Postcode</label>
<input
type="text"
id="postcode"
:value="postcode"
@input="$emit('update:postcode', $event.target.value)"
required
/>
</div>
</section>
</template>
<script>
export default {
name: "AddressFieldGroup",
props: {
label: {
type: String,
default: "",
},
streetName: {
type: String,
default: "",
},
streetNumber: {
type: String,
default: "",
},
city: {
type: String,
default: "",
},
postcode: {
type: String,
default: "",
},
},
};
</script>
In the above code, the section element with class name address is reused (as we’ll see a little later in this article) to create the Billing Address and Delivery Address in the parent component.
The label prop gives each address section its relevant name and four input fields: streetName
, streetNumber
, city
, and postcode
. The props for each input field along with the label are defined in the script tag.
The label prop will be passed from the custom component, AddressFieldGroup
, to its parent component in the App.vue
file in order to provide each address group with a unique label or name (e.g., Billing Address or Delivery Address).
Creating the CheckoutForm
Now, we’ll create the Checkout Form inside our App.vue
file and import the AddressFieldGroup.vue
into the App.vue
file as well:
<template>
<div class="app">
<form @submit.prevent="handleSubmit" class="checkout-form">
<h1>Checkout Form</h1>
<div class="address__field">
<label for="firstName">First name</label>
<input type="text" id="firstName" v-model="form.firstName" required />
</div>
<div class="address__field">
<label for="lastName">Last name</label>
<input type="text" id="lastName" v-model="form.lastName" required />
</div>
<div class="address__field">
<label for="email">Email</label>
<input type="email" id="email" v-model="form.email" required />
</div>
<AddressFieldGroup
label="Billing Address"
v-model:streetName="form.billingAddress.streetName"
v-model:streetNumber="form.billingAddress.streetNumber"
v-model:city="form.billingAddress.city"
v-model:postcode="form.billingAddress.postcode"
/>
<AddressFieldGroup
label="Delivery Address"
v-model:streetName="form.deliveryAddress.streetName"
v-model:streetNumber="form.deliveryAddress.streetNumber"
v-model:city="form.deliveryAddress.city"
v-model:postcode="form.deliveryAddress.postcode"
/>
<div class="address__field">
<button type="submit">Submit</button>
</div>
</form>
</div>
</template>
<script>
import AddressFieldGroup from "./components/AddressFieldGroup";
import { reactive } from "vue";
export default {
name: "CheckoutForm",
components: {
AddressFieldGroup: AddressFieldGroup,
},
methods: {
handleSubmit() {
alert("form submitted");
},
},
setup() {
const form = reactive({
firstName: "",
lastName: "",
email: "",
billingAddress: {
streetName: "",
streetNumber: "",
city: "",
postcode: "",
},
deliveryAddress: {
streetName: "",
streetNumber: "",
city: "",
postcode: "",
},
});
return {
form,
};
},
};
</script>
<style lang="scss">
.app {
font-family: Arial, Helvetica, sans-serif;
color: #434141;
text-align: center;
}
.checkout-form {
margin: 5px auto;
padding: 10px;
max-width: 500px;
display: flex;
flex-direction: column;
align-items: center;
}
.address__field {
padding-bottom: 10px;
width: 250px;
text-align: left;
}
label {
display: block;
font-weight: bold;
}
input {
padding: 10px;
width: 230px;
border: 1px solid #fff;
border-radius: 5px;
outline: 0;
background: #f8edcf;
}
button {
margin-top: 30px;
padding: 10px;
width: 250px;
color: #f8edcf;
border: 1px solid #fff;
border-radius: 5px;
outline: 0;
background: #434141;
}
</style>
In the above code, we’ve created a CheckoutForm that contains three input fields: firstName
, lastName
, and email
. We’ve also embedded the reusable AddressFieldGroup
component twice in the form and used it to represent both the user’s Billing Address and Delivery Address.
We used the v-model:{property-name}
format to bind every property on both custom AddressFieldGroup components.
In addition to the v-model shorthand syntax, this code is also shorter, simpler, and easier to read. This enables us to quickly decipher and decode the properties that are being passed between the parent component and the custom component (in this case, the reusable AddressFieldGroup component).
We also defined all properties in the CheckoutForm, including the properties of both addresses. We saved the properties inside a reactive object called form, returned its value to the component, and used it to set the bindings on the CheckoutForm.
Conclusion
In this article, we’ve explored the v-model directive, identified what Vue modifiers may be used with it, and demonstrated how to use multiple v-model bindings on Vue components to simplify the creation of complex Vue forms.
v-model gives us the flexibility to add multiple v-model directives on a single component instance and the **modelValue **can also be renamed according to our preference.
To view and play around with the example used in this article, check out the source code on CodeSandbox. Till next time guys, happy Vueing!
Connect with Me 🔗
Follow me on Twitter, LinkedIn, and Instagram to stay updated with my latest content. If you like my notes and want to support me, you can buy me a coffee on ByMeACoffee or GetFidia. I love the taste of coffee. 😍 For other ways to support me, visit my Sponsorship Page or Partnership Page.