Chapter 79: Vue v-model Directive
V-model
This is the directive that makes form handling in Vue feel almost too easy — it is the reason many developers fall in love with Vue in the first 10 minutes.
v-model is two-way data binding — it automatically keeps an input (or select, textarea, checkbox, custom component…) in sync with a piece of reactive data.
In plain words:
You type in the input → the data variable updates instantly You change the data variable in code → the input updates instantly
No manual value=”…” + @input=”…” glue code needed.
1. How v-model Works Under the Hood (Very Important to Understand)
For a normal <input type=”text”>:
|
0 1 2 3 4 5 6 |
<input v-model="message" /> |
Vue compiles this into two things:
- :value=”message” (binds the current value of message to the input)
- @input=”message = $event.target.value” (listens to every input event and updates message)
So the full expanded version is exactly this:
|
0 1 2 3 4 5 6 7 8 9 |
<input :value="message" @input="message = $event.target.value" /> |
Vue just gives you a beautiful shorthand so you don’t have to write both lines every time.
2. v-model Behavior on Different Input Types
Vue automatically chooses the correct prop and event depending on the element type.
| Element / Type | Prop used by v-model | Event used by v-model | Special notes / modifiers |
|---|---|---|---|
| <input type=”text”> | value | input | .trim, .lazy, .number |
| <textarea> | value | input | .trim, .lazy |
| <input type=”checkbox”> (single) | checked | change | boolean value |
| <input type=”checkbox”> (multiple) | checked | change | array of values |
| <input type=”radio”> | checked | change | string / number value |
| <select> (single) | value | change | string / number |
| <select multiple> | value | change | array |
| <input type=”number”> | valueAsNumber | input | Use .number modifier! |
| Custom component | modelValue | update:modelValue | Standard v-model pattern |
3. Real, Practical Example – Complete Signup Form (2026 Style)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
<template> <form @submit.prevent="handleSubmit" class="signup-form"> <!-- Text input + .trim + .lazy --> <div class="field"> <label>Full Name</label> <input v-model.trim.lazy="form.name" placeholder="Rahul Sharma" required /> <small v-if="form.name && form.name.length < 3" class="error"> Minimum 3 characters </small> </div> <!-- Email input --> <div class="field"> <label>Email</label> <input v-model.trim="form.email" type="email" placeholder="you@company.com" required /> </div> <!-- Password with show/hide toggle --> <div class="field password-field"> <label>Password</label> <input :type="showPassword ? 'text' : 'password'" v-model.trim="form.password" placeholder="••••••••" required minlength="8" /> <button type="button" class="toggle-btn" @click="showPassword = !showPassword" > {{ showPassword ? 'Hide' : 'Show' }} </button> </div> <!-- Number input + .number modifier --> <div class="field"> <label>Age</label> <input v-model.number="form.age" type="number" min="18" max="120" /> </div> <!-- Single checkbox --> <div class="field checkbox"> <input id="newsletter" type="checkbox" v-model="form.subscribe" /> <label for="newsletter">Subscribe to newsletter</label> </div> <!-- Multiple checkboxes (array) --> <div class="field"> <label>Interests:</label> <label v-for="interest in interests" :key="interest"> <input type="checkbox" :value="interest" v-model="form.interests" /> {{ interest }} </label> </div> <!-- Radio buttons --> <div class="field"> <label>Gender:</label> <label> <input type="radio" v-model="form.gender" value="male" /> Male </label> <label> <input type="radio" v-model="form.gender" value="female" /> Female </label> </div> <!-- Select single --> <div class="field"> <label>Country</label> <select v-model="form.country"> <option value="">Select country</option> <option value="IN">India</option> <option value="US">USA</option> <option value="UK">United Kingdom</option> </select> </div> <!-- Submit button – disabled until valid --> <button type="submit" :disabled="!formIsValid || isSubmitting" > {{ isSubmitting ? 'Submitting…' : 'Sign Up' }} </button> <p v-if="submitError" class="error">{{ submitError }}</p> </form> </template> <script setup lang="ts"> import { reactive, ref, computed } from 'vue' const form = reactive({ name: '', email: '', password: '', age: null as number | null, subscribe: true, interests: [] as string[], gender: '', country: '' }) const interests = ['Vue.js', 'TypeScript', 'Tailwind', 'Node.js'] const showPassword = ref(false) const isSubmitting = ref(false) const submitError = ref<string | null>(null) const formIsValid = computed(() => { return ( form.name.trim().length >= 3 && form.email.includes('@') && form.password.length >= 8 && form.age !== null && form.age >= 18 && form.country ) }) async function handleSubmit() { if (!formIsValid.value) return isSubmitting.value = true submitError.value = null try { // Fake API call await new Promise(r => setTimeout(r, 1500)) alert('Signup successful! 🎉') // router.push('/dashboard') in real app } catch (err: any) { submitError.value = err.message || 'Signup failed. Please try again.' } finally { isSubmitting.value = false } } </script> <style scoped> .signup-form { max-width: 480px; margin: 3rem auto; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .field { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 500; } input, select { width: 100%; padding: 0.9rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 1rem; } .password-field { position: relative; } .toggle-btn { position: absolute; right: 1rem; top: 50%; transform: translateY(-50%); background: none; border: none; color: #3b82f6; cursor: pointer; } .checkbox { display: flex; align-items: center; gap: 0.5rem; } button { width: 100%; padding: 0.9rem; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; margin-top: 1rem; } button:disabled { opacity: 0.6; cursor: not-allowed; } .error { color: #dc2626; margin-top: 1rem; text-align: center; } </style> |
Quick Summary Table – v-model Modifiers & Behavior
| Input Type | v-model Prop | v-model Event | Useful Modifiers | Default Value Type |
|---|---|---|---|---|
| text / email / tel | value | input | .trim, .lazy, .number | string |
| number | valueAsNumber | input | .number (strongly recommended) | number |
| checkbox (single) | checked | change | — | boolean |
| checkbox (multiple) | checked | change | — | array |
| radio | checked | change | — | string / number |
| select (single) | value | change | — | string / number |
| select (multiple) | value | change | — | array |
| Custom component | modelValue | update:modelValue | .trim, .number, .lazy | any |
Pro Tips from Real Projects (Hyderabad 2026)
-
Always use .trim on text inputs — prevents trailing spaces
-
Use .number on <input type=”number”> — otherwise you get string
-
Use .lazy on inputs where you don’t need live updates (saves performance)
-
For custom components → implement modelValue prop + update:modelValue emit → enables v-model
-
Use multiple v-model on custom components (Vue 3.2+)
vue0123456<MyEditor v-model:title="post.title" v-model:content="post.content" /> -
Debounce heavy inputs (live search) → use watch + setTimeout or lodash.debounce
-
Form libraries (VeeValidate, FormKit, Vorms) → handle v-model + validation beautifully
Your mini practice task:
- Build the signup form above
- Add validation messages with v-if
- Add .trim and .lazy to name/email
- Add multiple checkboxes and radio buttons
- Disable submit button until form is valid
Any part confusing? Want full examples for:
- Custom component with v-model support?
- Multiple v-model on same component?
- Debounced search with watch + v-model?
- File input (no v-model – use @change)?
Just tell me — we’ll build the next production-ready form together 🚀
