Vue.js 3 Two-way Databinding Tutorial

In this Vue tutorial we learn how to combine string interpolation and event binding to react to events and output data at the same time, a process known as Two-way Databinding.

We cover the binding directive, modifiers, binding various form elements and the basics of form handling.

Lesson Project

If you want to follow along with the examples in this lesson, you will need an app generated by the Vue CLI . If you already have one from the previous lesson, you can use it instead.

Two-way databinding with v-model

Two-way databinding combines string interpolation and event binding to react to events and output data at the same time. This ensures that the Logic and the View is always in sync.

Two-way databinding uses the v-model directive on the element we want to take input from, like a text field. As its value, it references a data property from the Logic that we want to modify.

Syntax: v-model
<element v-model="data_property" />

As an example, let’s say we have a text field that requests a user’s name and stores it in a data property.

Example: src/App.vue
<template>
  <p>Name: {{ name }}</p>
  <input type="text" v-model="name">
</template>

<script>
export default {
  data() {
    return {
      name: ''
    }
  }
}
</script>

If we type a name in the text field in the browser, it will automatically update the paragraph above it.

If you remember from the lesson on event handling , we had to do the same thing in a two step process where we used a method to modify the data property.

Example: src/App.vue
<template>
  <p>Your name: {{ name }}</p>
  <input type="text" @input="getInput">
</template>

<script>
export default {
  data() {
    return {
      name: ''
    }
  },
  methods: {
    getInput() {
      this.name = event.target.value
    }
  }
}
</script>

With v-model , the data property is directly connected to the input so we don’t need the method.

The lazy modifier

By default, Vue will evaluate v-model every time the input event is fired, either on each keypress or a text paste. We can wait until the user has finished inputting and has unfocused the field by chaining the lazy modifier to v-model .

To demonstrate, we’ll expand our example to use a first and last name. The first name will have the lazy modifier, the last name will be the standard binding.

Example: src/App.vue
<template>
  <p>Lazy First Name: {{ firstName }}</p>
  <input type="text" v-model.lazy="firstName">

  <p>Realtime Last Name: {{ lastName }}</p>
  <input type="text" v-model="lastName">
</template>

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  }
}
</script>

When we type a name into the first field, nothing will show until we switch over to the second field. The second field will show each character we type in real time.

Technically, the lazy modifier syncs the changes on the change event instead of the input event, which fires less frequently. This can help speed up the application because Vue doesn’t have to render each time a key is pressed.

The trim modifier

Sometimes, users will accidentally leave whitespace before or after their input. We can do a quick tidy up operation that removes the whitespace by using the trim modifier.

tip Event modifiers can be chained to each other so v-model.lazy.trim is legal and will work fine.

Let’s add the trim modifier to both inputs in our earlier example.

Example: src/App.vue
<template>
  <p>Lazy First Name: {{ firstName }}</p>
  <input type="text" v-model.lazy.trim="firstName">

  <p>Realtime Last Name: {{ lastName }}</p>
  <input type="text" v-model.trim="lastName">
</template>

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  }
}
</script>

If we add any spaces before or after the text we type, Vue will strip them when we unfocus the field.

Two-way databinding form elements

Let’s take a look at more form elements and how to use v-model with them, starting with the textarea.

Two-way databinding with a Textarea

Binding data from a textarea input element is exactly the same as with a text field.

Example: src/App.vue
<template>
  <p>Personal Profile: {{ profile }}</p>
  <textarea cols="30" rows="10" v-model="profile"></textarea>
</template>

<script>
export default {
  data() {
    return {
      profile: ''
    }
  }
}
</script>

Anything we type in the textarea will be shown in the paragraph.

tip Because textareas allow for a large amount of characters, consider optimizing performance with the lazy modifier.

Two-way databinding with Single-Select Dropdowns

When we use a grouped element, we specify the v-model directive on the parent.

Single-select dropdown elements use select as the parent and option as the children.

Example: src/App.vue
<template>
  <p>Country: {{ country }}</p>
  <select v-model="country">
    <option value="">Please select your Coutry</option>
    <option value="US">United States</option>
    <option value="UK">United Kingdom</option>
    <option value="AU">Australia</option>
  </select>
</template>

<script>
export default {
  data() {
    return {
      country: ''
    }
  }
}
</script>

When we choose an option in the dropdown, it will display in the paragraph.

note We chose to have the first option’s value empty. This allows us to create logic to check if the user has actively chosen an option, which is useful when a dropdown is mandatory on a form.

Two-way databinding with Multi-Select Controls

When we use a multi-select control, the data must be stored in a multi-value data container, like an array.

A multi-select control uses select as the parent and option as the children, but adds the multiple attribute to allow more than one selection.

note To keep this example simple, we use the Javascript join method to display the items nicely. Typically this would be done with Vue’s built-in iteration rendering directives .

Example: src/App.vue
<template>
  <p>Countries Visited: {{ visited.join(', ') }}</p>
  <select multiple v-model="visited">
    <option value="US">United States</option>
    <option value="UK">United Kingdom</option>
    <option value="AU">Australia</option>
    <option value="NZ">New Zealand</option>
  </select>
</template>

<script>
export default {
  data() {
    return {
      visited: []
    }
  }
}
</script>

When we ctrl + click elements in the multi-select control, it will display those elements in the paragraph.

note Only items that have been checked will be added to the array, there is no “unchecked” value.

Also, items will be added to the array in the same order that they are listed. This may be important for your form and how you process data sent to a storage layer.

Two-way databinding with Radio Buttons

A radio button element works similar to a text input field. We bind whatever is in the value attribute.

Example: src/App.vue
<template>
  <p>Rate this page: {{ rating }}</p>

  <label><input type="radio" name="rating" value="Bad"  v-model="rating" />  Bad</label><br>
  <label><input type="radio" name="rating" value="Okay" v-model="rating" /> Okay</label><br>
  <label><input type="radio" name="rating" value="Good" v-model="rating" /> Good</label>
</template>

<script>
export default {
  data() {
    return {
      rating: ''
    }
  }
}
</script>

When we select a radio button on the page, it will display Bad, Okay or Good in the paragraph.

note If we use a modifier like lazy , we need to specify it on each individual radio button.

Two-way databinding with Checkboxes

Checkboxes work a little different. A single checkbox without a value attribute will return a boolean value of either true or false.

As an example, let’s give the user an option to subscribe to a newsletter by checking a checkbox.

Example: src/App.vue
<template>
  <p>Subscribed: {{ subscribe }}</p>

  <label>
    <input type="checkbox" v-model="subscribe" />
    Do you want to subscribe to our newsletter?
  </label>
</template>

<script>
export default {
  data() {
    return {
      subscribe: false
    }
  }
}
</script>
Example: src/App.vue
<template>
  <p>Subscribed: {{ subscribe }}</p>

  <label>
    <input type="checkbox" v-model="subscribe" checked/>
    Do you want to subscribe to our newsletter?
  </label>
</template>

<script>
export default {
  data() {
    return {
      subscribe: true
    }
  }
}
</script>

When we check and uncheck the checkbox above, it will display true and false respectively.

A checkbox’s default state is unchecked but we can add the checked attribute to make the default state checked. This means we also have to change the value of subscribe to be true by default.

Two-way databinding with a Checkbox Group

If we have multiple checkboxes, the contents of their value property can be sent to an array if they are checked.

As an example, let’s simulate a shopping list that has been populated with some shopping items. The user can then check items as they collect them in their cart.

note To keep this example simple, we use the Javascript join method to display the items nicely. Typically this would be done with Vue’s built-in iteration rendering directives .

Example: src/App.vue
<template>
  <p>Done: {{ shoppingList.join(', ') }}</p>

  <label><input type="checkbox" value="Bread" v-model="shoppingList" /> Bread</label><br>
  <label><input type="checkbox" value="Milk"  v-model="shoppingList" /> Milk</label><br>
  <label><input type="checkbox" value="Eggs"  v-model="shoppingList" /> Eggs</label>
</template>

<script>
export default {
  data() {
    return {
      shoppingList: []
    }
  }
}
</script>

note Only items that have been checked will be added to the array, there is no “unchecked” value.

Also, items will be added to the array in the same order that they are listed. This may be important for your form and how you process data sent to a storage layer.

Form Handling Basics

Now that we understand how to work with various form controls, let’s quickly discuss how to submit the data.

All we need to do is add a submit event to our form that references a method. The method is repsonsible for performing any operations on the data, like validating it and/or sending it to a storage layer.

Example: src/App.vue
<template>
  <p>Name: {{ formData.firstName }} {{ formData.lastName }}</p>
  <form @submit.prevent="submitForm">
    <p>
      <label for="firstName">First Name:</label>
      <input id="firstName" type="text" v-model.lazy.trim="formData.firstName">
    </p>
    <p>
      <label for="lastName">Last Name:</label>
      <input id="lastName" type="text" v-model.lazy.trim="formData.lastName">
    </p>
    <button>Submit Form</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        firstName: '',
        lastName: ''
      }
    };
  },
  methods: {
    submitForm() {
      console.log('Form Data', this.formData)
    }
  }
}
</script>

When we enter a first and last name in the input fields and submit the form, Vue will invoke the submitForm method and log the object to the console.

Output:
Form Data > Proxy {firstName: "John", lastName: "Doe"}

Usually, the data would be sent to an external resource like Firebase but we’ll just log it to the console to keep things simple for now.