Using v-model in Vue 3 to build complex forms - LogRocket Blog (2024)

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.

Using v-model in Vue 3 to build complex forms - LogRocket Blog (1)

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 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 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.

Over 200k developers use LogRocket to create better digital experiencesLearn more →

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.

Using v-model in Vue 3 to build complex forms - LogRocket Blog (4)

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:

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.

Using v-model in Vue 3 to build complex forms - LogRocket Blog (2024)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Foster Heidenreich CPA

Last Updated:

Views: 6775

Rating: 4.6 / 5 (76 voted)

Reviews: 83% of readers found this page helpful

Author information

Name: Foster Heidenreich CPA

Birthday: 1995-01-14

Address: 55021 Usha Garden, North Larisa, DE 19209

Phone: +6812240846623

Job: Corporate Healthcare Strategist

Hobby: Singing, Listening to music, Rafting, LARPing, Gardening, Quilting, Rappelling

Introduction: My name is Foster Heidenreich CPA, I am a delightful, quaint, glorious, quaint, faithful, enchanting, fine person who loves writing and wants to share my knowledge and understanding with you.