Skip to main content

Vue.js Input Bindings

Introduction

When building interactive web applications, handling user input is a fundamental requirement. Vue.js makes this process intuitive and efficient through a feature called input bindings. At its core, input binding allows you to create a two-way connection between form inputs and your application's state, enabling you to both display data to users and capture their input updates.

In this tutorial, we'll explore how to use Vue.js input bindings to create responsive forms that react to user input in real-time. We'll focus on the v-model directive, which is Vue's built-in solution for two-way data binding.

Understanding v-model

The v-model directive creates a two-way binding between a form input and a piece of data in your Vue component. When the data changes, the input updates, and when the user modifies the input, the data changes.

Behind the scenes, v-model is syntactic sugar that combines:

  1. A :value binding (to set the initial value)
  2. An @input event listener (to update the value when it changes)

Let's start with a basic example:

html
<template>
<div>
<label for="username">Username:</label>
<input id="username" v-model="username">
<p>Your username is: {{ username }}</p>
</div>
</template>

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

In this example, whenever the user types in the input field, the username data property is automatically updated, and the text below the input instantly reflects this change.

Input Types and Their Bindings

v-model works with various form input types, but each behaves slightly differently. Let's explore the most common ones:

Text Inputs

For standard text inputs, v-model binds to the input's value:

html
<template>
<div>
<input v-model="message">
<p>Message: {{ message }}</p>
</div>
</template>

<script>
export default {
data() {
return {
message: 'Hello Vue!'
}
}
}
</script>

Textarea

Textareas work similarly to text inputs:

html
<template>
<div>
<textarea v-model="description"></textarea>
<p>Description ({{ description.length }} characters):</p>
<p>{{ description }}</p>
</div>
</template>

<script>
export default {
data() {
return {
description: 'Write your description here...'
}
}
}
</script>

Checkboxes

For a single checkbox, v-model binds to a boolean value:

html
<template>
<div>
<input type="checkbox" id="subscribe" v-model="subscribed">
<label for="subscribe">Subscribe to newsletter</label>
<p>Subscription status: {{ subscribed ? 'Subscribed' : 'Not subscribed' }}</p>
</div>
</template>

<script>
export default {
data() {
return {
subscribed: false
}
}
}
</script>

For multiple checkboxes, v-model can bind to an array:

html
<template>
<div>
<h3>Which technologies do you use?</h3>
<input type="checkbox" id="vue" value="Vue" v-model="technologies">
<label for="vue">Vue</label>
<input type="checkbox" id="react" value="React" v-model="technologies">
<label for="react">React</label>
<input type="checkbox" id="angular" value="Angular" v-model="technologies">
<label for="angular">Angular</label>
<p>Selected technologies: {{ technologies.join(', ') }}</p>
</div>
</template>

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

Radio Buttons

Radio buttons bind to a single value:

html
<template>
<div>
<h3>Select your experience level:</h3>
<input type="radio" id="beginner" value="beginner" v-model="experienceLevel">
<label for="beginner">Beginner</label>
<input type="radio" id="intermediate" value="intermediate" v-model="experienceLevel">
<label for="intermediate">Intermediate</label>
<input type="radio" id="advanced" value="advanced" v-model="experienceLevel">
<label for="advanced">Advanced</label>
<p>Your experience level: {{ experienceLevel }}</p>
</div>
</template>

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

Select Dropdowns

For select elements, v-model binds to the selected option's value:

html
<template>
<div>
<label for="country">Country:</label>
<select id="country" v-model="selectedCountry">
<option value="">Please select a country</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="UK">United Kingdom</option>
</select>
<p>Selected country: {{ selectedCountry || 'None' }}</p>
</div>
</template>

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

For multi-select (with the multiple attribute), v-model binds to an array:

html
<template>
<div>
<label for="languages">Programming languages:</label>
<select id="languages" v-model="selectedLanguages" multiple>
<option value="js">JavaScript</option>
<option value="py">Python</option>
<option value="java">Java</option>
<option value="csharp">C#</option>
</select>
<p>Selected languages: {{ selectedLanguages.join(', ') }}</p>
</div>
</template>

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

Modifiers for v-model

Vue provides several modifiers to enhance how v-model works:

.lazy

By default, v-model syncs the input with the data after each input event. With the .lazy modifier, it syncs after change events instead (typically when the input loses focus):

html
<input v-model.lazy="message">

.number

If you want to automatically typecast the input value as a number, use the .number modifier:

html
<input v-model.number="age" type="number">

.trim

The .trim modifier automatically removes whitespace from the beginning and end of the input:

html
<input v-model.trim="username">

You can even combine modifiers:

html
<input v-model.number.trim="price">

Real-world Example: Registration Form

Let's build a more complex example - a user registration form:

html
<template>
<div class="registration-form">
<h2>User Registration</h2>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="fullName">Full Name:</label>
<input
id="fullName"
v-model.trim="user.fullName"
type="text"
required
>
</div>

<div class="form-group">
<label for="email">Email:</label>
<input
id="email"
v-model="user.email"
type="email"
required
>
</div>

<div class="form-group">
<label for="age">Age:</label>
<input
id="age"
v-model.number="user.age"
type="number"
min="18"
>
</div>

<div class="form-group">
<label for="bio">Bio:</label>
<textarea
id="bio"
v-model="user.bio"
rows="4"
></textarea>
</div>

<div class="form-group">
<label>Interests:</label>
<div>
<input type="checkbox" id="tech" value="technology" v-model="user.interests">
<label for="tech">Technology</label>
</div>
<div>
<input type="checkbox" id="sports" value="sports" v-model="user.interests">
<label for="sports">Sports</label>
</div>
<div>
<input type="checkbox" id="art" value="art" v-model="user.interests">
<label for="art">Art</label>
</div>
</div>

<div class="form-group">
<label>Preferred Contact Method:</label>
<div>
<input type="radio" id="emailContact" value="email" v-model="user.contactMethod">
<label for="emailContact">Email</label>
</div>
<div>
<input type="radio" id="phoneContact" value="phone" v-model="user.contactMethod">
<label for="phoneContact">Phone</label>
</div>
</div>

<div class="form-group">
<label for="country">Country:</label>
<select id="country" v-model="user.country">
<option value="">-- Select country --</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="UK">United Kingdom</option>
<option value="AU">Australia</option>
</select>
</div>

<div class="form-group">
<input type="checkbox" id="terms" v-model="user.agreeToTerms">
<label for="terms">I agree to the terms and conditions</label>
</div>

<button type="submit" :disabled="!formIsValid">Register</button>
</form>

<div v-if="formSubmitted" class="form-summary">
<h3>Registration Summary:</h3>
<pre>{{ JSON.stringify(user, null, 2) }}</pre>
</div>
</div>
</template>

<script>
export default {
data() {
return {
formSubmitted: false,
user: {
fullName: '',
email: '',
age: null,
bio: '',
interests: [],
contactMethod: 'email',
country: '',
agreeToTerms: false
}
}
},
computed: {
formIsValid() {
return (
this.user.fullName.length > 0 &&
this.user.email.length > 0 &&
this.user.agreeToTerms
)
}
},
methods: {
submitForm() {
// In a real app, we would send data to the server here
this.formSubmitted = true;
console.log('Form submitted:', this.user);
}
}
}
</script>

<style scoped>
.registration-form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="email"],
input[type="number"],
textarea,
select {
width: 100%;
padding: 8px;
}
button {
background: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
cursor: pointer;
}
button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.form-summary {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border: 1px solid #ddd;
}
</style>

In this example, we have various form controls all bound with v-model to a single user object. We also use computed properties to validate the form before submission, showing how form data and UI state can work together.

Understanding Value Binding vs v-model

To truly understand the power of v-model, let's see how it compares to the more manual approach:

html
<!-- Using v-model (concise two-way binding) -->
<input v-model="message">

<!-- Equivalent without v-model -->
<input
:value="message"
@input="message = $event.target.value"
>

The second approach explicitly shows what v-model does behind the scenes: it updates the message property whenever the input event fires.

Component v-model

Vue allows you to use v-model with custom components too. This is particularly useful for creating reusable form components. Here's how you can create a custom input component with v-model support:

html
<!-- CustomInput.vue -->
<template>
<div class="custom-input">
<label v-if="label">{{ label }}</label>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
v-bind="$attrs"
>
</div>
</template>

<script>
export default {
name: 'CustomInput',
props: {
modelValue: {
type: [String, Number],
default: ''
},
label: {
type: String,
default: ''
}
},
emits: ['update:modelValue']
}
</script>

You can then use it with v-model in another component:

html
<template>
<div>
<custom-input
v-model="username"
label="Username:"
placeholder="Enter your username"
/>
<p>Your username: {{ username }}</p>
</div>
</template>

<script>
import CustomInput from './CustomInput.vue'

export default {
components: {
CustomInput
},
data() {
return {
username: ''
}
}
}
</script>

Summary

Input bindings in Vue.js, primarily implemented through the v-model directive, create a seamless two-way connection between form inputs and your component's data. Key takeaways include:

  • v-model provides a concise syntax for two-way data binding
  • Different input types (text, checkbox, radio, select) work with v-model in slightly different ways
  • Modifiers like .lazy, .number, and .trim extend v-model's functionality
  • Custom components can support v-model through props and events

By effectively using input bindings, you can create responsive and interactive forms that synchronize with your application's state without writing verbose update handlers.

Exercises

  1. Create a simple feedback form with name, email, rating (1-5), and comments fields, all using v-model.
  2. Build a dynamic form where checking a checkbox reveals additional form fields.
  3. Create a custom toggle switch component that works with v-model.
  4. Build a form with validation that highlights fields with errors when they lose focus.
  5. Create a multi-step form wizard where data is collected across several screens but maintained in a single data object.

Additional Resources

Remember that mastering input bindings is a fundamental step in building interactive Vue applications. The techniques covered in this tutorial will serve as a foundation for more complex form handling in your future Vue projects.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)