Vue Forms — v-model
v-model — Two-Way Binding
v-model creates a two-way binding between a form input and reactive data. It's shorthand for :value="data" + @input="data = $event.target.value". Vue's v-model is much simpler than React's controlled components.
<template>
<form @submit.prevent="handleSubmit">
<!-- Text input -->
<input v-model="form.name" placeholder="Name" />
<!-- Email -->
<input v-model="form.email" type="email" placeholder="Email" />
<!-- Number with .number modifier -->
<input v-model.number="form.age" type="number" />
<!-- Textarea -->
<textarea v-model="form.bio" rows="4" />
<!-- Select -->
<select v-model="form.country">
<option value="">Select country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="in">India</option>
</select>
<!-- Multi-select -->
<select v-model="form.skills" multiple>
<option v-for="skill in availableSkills" :key="skill" :value="skill">
{{ skill }}
</option>
</select>
<!-- Radio buttons -->
<label v-for="role in roles" :key="role">
<input type="radio" v-model="form.role" :value="role" />
{{ role }}
</label>
<!-- Checkbox (boolean) -->
<label>
<input type="checkbox" v-model="form.newsletter" />
Subscribe to newsletter
</label>
<!-- Checkbox (array) -->
<label v-for="tag in availableTags" :key="tag">
<input type="checkbox" v-model="form.tags" :value="tag" />
{{ tag }}
</label>
<button type="submit">Submit</button>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</form>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
name: '', email: '', age: 0, bio: '',
country: '', skills: [], role: 'user',
newsletter: false, tags: []
})
const availableSkills = ['Vue', 'React', 'Angular', 'Node.js']
const roles = ['user', 'admin', 'moderator']
const availableTags = ['Frontend', 'Backend', 'DevOps', 'Design']
function handleSubmit() {
console.log('Form submitted:', form)
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div class="field">
<input v-model="form.email" type="email" placeholder="Email"
:class="{ 'input-error': errors.email }" @blur="validateField('email')" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<div class="field">
<input v-model="form.password" type="password" placeholder="Password"
:class="{ 'input-error': errors.password }" @blur="validateField('password')" />
<span v-if="errors.password" class="error">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="!isValid">Login</button>
</form>
</template>
<script setup>
import { reactive, computed } from 'vue'
const form = reactive({ email: '', password: '' })
const errors = reactive({ email: '', password: '' })
function validateField(field) {
if (field === 'email') {
if (!form.email) errors.email = 'Email is required'
else if (!/\S+@\S+\.\S+/.test(form.email)) errors.email = 'Invalid email'
else errors.email = ''
}
if (field === 'password') {
if (!form.password) errors.password = 'Password is required'
else if (form.password.length < 8) errors.password = 'Min 8 characters'
else errors.password = ''
}
}
const isValid = computed(() =>
form.email && form.password && !errors.email && !errors.password
)
function handleSubmit() {
validateField('email')
validateField('password')
if (isValid.value) alert('Login successful!')
}
</script>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.