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:
- A
:value
binding (to set the initial value) - An
@input
event listener (to update the value when it changes)
Let's start with a basic example:
<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:
<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:
<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:
<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:
<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:
<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:
<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:
<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):
<input v-model.lazy="message">
.number
If you want to automatically typecast the input value as a number, use the .number
modifier:
<input v-model.number="age" type="number">
.trim
The .trim
modifier automatically removes whitespace from the beginning and end of the input:
<input v-model.trim="username">
You can even combine modifiers:
<input v-model.number.trim="price">
Real-world Example: Registration Form
Let's build a more complex example - a user registration form:
<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:
<!-- 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:
<!-- 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:
<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
extendv-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
- Create a simple feedback form with name, email, rating (1-5), and comments fields, all using
v-model
. - Build a dynamic form where checking a checkbox reveals additional form fields.
- Create a custom toggle switch component that works with
v-model
. - Build a form with validation that highlights fields with errors when they lose focus.
- Create a multi-step form wizard where data is collected across several screens but maintained in a single data object.
Additional Resources
- Vue.js Form Input Bindings (Official Documentation)
- Vue.js Component v-model (Official Documentation)
- Vue.js Form Validation with Vuelidate
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! :)