In this article, we’ll introduce changes to the v-model
directive in Vue 3. Then, we’ll step through a tutorial, demonstrating how to use multiple v-model
bindings to simplify the process of building complex forms in Vue.
Jump ahead:
- What is the v-model directive?
- How does v-model handle data binding?
- How is the v-model in Vue.js 3 different from Vue.js 2?
- Multiple v-model directive bindings tutorial
- Creating the reusable component
- Creating the CheckoutForm
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 andinput
event fortext
andtextarea
elementschecked
property andchange
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 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 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 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-mode
l, 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:
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.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to use the React children prop with TypeScript
- Explore creating a custom mouse cursor with CSS
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
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:
App.vue
<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. Happy Vueing!
Experience your Vue apps exactly how a user does
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.