Chapter 12: Vue Forms
Vue Forms in detail π
Forms are one of the most common things you’ll build in any web app: login, signup, contact, checkout, settings, todo add/edit, etc. Vue makes forms feel natural because of v-model (two-way binding), but in 2026 with Vue 3 + Composition API + <script setup>, we have clean, powerful patterns.
“Vue Forms” really means:
- Binding inputs with v-model
- Handling different input types (text, checkbox, radio, select, file, etc.)
- Collecting & submitting data
- Basic validation (required, email, min-lengthβ¦)
- Showing errors nicely
- Best practices for clean, reusable, scalable forms
No heavy libraries today β we’ll do it vanilla Vue 3 first (good foundation), then mention popular helpers like vee-validate or Vorms at the end.
1. Core of Vue Forms: v-model (Two-Way Binding)
v-model is sugar that does:
- :value=”variable” + @input=”variable = $event.target.value”
Modern shorthand works on almost everything.
Basic login form example (2026 style with <script setup>):
|
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 |
<!-- LoginForm.vue --> <template> <form @submit.prevent="handleLogin" class="login-form"> <div class="field"> <label for="email">Email</label> <input id="email" type="email" v-model="form.email" placeholder="you@example.com" required /> </div> <div class="field"> <label for="password">Password</label> <input id="password" :type="showPassword ? 'text' : 'password'" v-model="form.password" placeholder="β’β’β’β’β’β’β’β’" /> <button type="button" class="toggle" @click="showPassword = !showPassword" > {{ showPassword ? 'Hide' : 'Show' }} </button> </div> <button type="submit" :disabled="loading"> {{ loading ? 'Logging in...' : 'Login' }} </button> <p v-if="error" class="error">{{ error }}</p> </form> </template> <script setup> import { reactive, ref } from 'vue' const form = reactive({ email: '', password: '' }) const showPassword = ref(false) const loading = ref(false) const error = ref('') async function handleLogin() { if (!form.email || !form.password) { error.value = 'Please fill both fields!' return } loading.value = true error.value = '' try { // Fake API call await new Promise(resolve => setTimeout(resolve, 1200)) console.log('Logged in with:', form) alert('Welcome back! π') // router.push('/dashboard') } catch (err) { error.value = 'Invalid credentials. Try again.' } finally { loading.value = false } } </script> <style scoped> .login-form { max-width: 400px; margin: 2rem auto; padding: 2rem; border: 1px solid #ddd; border-radius: 12px; } .field { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 500; } input { width: 100%; padding: 0.8rem; border: 1px solid #ccc; border-radius: 6px; } .toggle { margin-left: 0.5rem; background: none; border: none; color: #3b82f6; cursor: pointer; } button[type="submit"] { width: 100%; padding: 0.9rem; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; } button:disabled { opacity: 0.6; cursor: not-allowed; } .error { color: #dc2626; margin-top: 1rem; text-align: center; } </style> |
Key lessons here:
- reactive({}) β perfect for form objects (deep reactivity)
- v-model on every input β auto-syncs value
- @submit.prevent β stops page reload (modifier magic!)
- Disable button during loading
- Show inline errors
2. Other Input Types with v-model
Vue handles them automatically:
|
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 |
<!-- Checkbox (boolean or array) --> <input type="checkbox" v-model="form.rememberMe" /> Remember me <!-- Multiple checkboxes --> <div> <label v-for="interest in interests" :key="interest"> <input type="checkbox" :value="interest" v-model="form.interests" /> {{ interest }} </label> </div> <!-- Radio --> <div> <label><input type="radio" value="male" v-model="form.gender" /> Male</label> <label><input type="radio" value="female" v-model="form.gender" /> Female</label> </div> <!-- Select --> <select v-model="form.country"> <option value="">Select country</option> <option value="IN">India</option> <option value="US">USA</option> <option value="UK">UK</option> </select> <!-- Multiple select --> <select multiple v-model="form.languages"> <option>English</option> <option>Hindi</option> <option>Telugu</option> </select> <!-- Number input (v-model.number modifier) --> <input type="number" v-model.number="form.age" /> |
Modifiers for v-model:
- .lazy β update on blur (not every keystroke)
- .number β parse as number
- .trim β trim whitespace
3. Simple Built-in Validation (No Library)
Use HTML5 + Vue reactivity:
|
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 |
<template> <form @submit.prevent="submit"> <input v-model.trim="form.email" type="email" required placeholder="Email" :class="{ 'invalid': !isEmailValid && form.email }" /> <small v-if="!isEmailValid && form.email" class="error"> Please enter a valid email </small> <input v-model="form.password" type="password" required minlength="8" :class="{ 'invalid': form.password.length < 8 && form.password }" /> <small v-if="form.password.length > 0 && form.password.length < 8" class="error"> Min 8 characters </small> <button type="submit" :disabled="!isFormValid">Register</button> </form> </template> <script setup> import { reactive, computed } from 'vue' const form = reactive({ email: '', password: '' }) const isEmailValid = computed(() => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email) }) const isFormValid = computed(() => { return isEmailValid.value && form.password.length >= 8 }) function submit() { console.log('Submitted:', form) } </script> <style scoped> .invalid { border-color: #dc2626 !important; } .error { color: #dc2626; font-size: 0.9rem; } </style> |
4. Reusable Form Components (Best Practice 2026)
Don’t repeat input + label + error everywhere.
Create <FormInput.vue>:
|
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 |
<!-- FormInput.vue --> <template> <div class="form-field"> <label :for="id">{{ label }}</label> <input :id="id" :type="type" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :placeholder="placeholder" :class="{ 'invalid': error }" /> <small v-if="error" class="error">{{ error }}</small> </div> </template> <script setup> defineProps({ modelValue: String, label: String, type: { type: String, default: 'text' }, placeholder: String, error: String }) defineEmits(['update:modelValue']) const id = `input-${Math.random().toString(36).slice(2)}` </script> |
Usage:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
<FormInput v-model="form.email" label="Email" type="email" placeholder="you@example.com" :error="errors.email" /> |
5. Modern Best Practices Summary (2026)
- Use reactive({}) for form state (or ref({}))
- Prefer <script setup>
- Always @submit.prevent
- Use computed for validation rules
- Show errors after blur or submit (use touched flag)
- Disable submit if invalid / loading
- For complex forms β consider vee-validate 4 (Composition API friendly) or Vorms
- For very large forms β Vueform or FormKit (component libraries)
Quick table β Input types & v-model behavior
| Input Type | v-model Behavior | Useful Modifiers |
|---|---|---|
| text/email | string | .trim, .lazy |
| number | number (with .number) | .number |
| checkbox | boolean / array | β |
| radio | string / number | β |
| select | string / array (multiple) | β |
| textarea | string | .trim, .lazy |
Practice task: Build a signup form with:
- Email + password + confirm password
- Checkbox “I agree to terms”
- Show match error on confirm password
- Submit β fake API delay β success message
Any part confusing? Want full multi-step wizard form? Or vee-validate + yup example? Or file upload handling?
Just tell me β we’ll go as deep as you want π
Happy form-building from Hyderabad! π
