Forms and Validation
Vue's v-model handles the mechanical part of forms — keeping an input's displayed value in sync with a piece of reactive state — but validation is a separate concern layered on top: deciding whether the current values are acceptable, when to tell the user they aren't, and how to prevent submission of invalid data. A well-built form distinguishes between raw field values, per-field error messages, and overall form validity, and it times error display carefully so users aren't scolded for a field they haven't finished typing into yet.
Cricket analogy: v-model keeps a scorecard field in sync like an alert scorer jotting runs the instant they're called, but validation is separate: deciding whether a claimed six is actually valid, when to flag a scoring dispute, and how to block a corrupted card from being submitted to the umpires.
Binding Fields With v-model
For a multi-field form, it's common to hold all field values in a single reactive() object rather than one ref per field, since this keeps related fields grouped and makes it trivial to serialize the whole form for submission. Each input binds via v-model="formState.fieldName", and Vue keeps the object's property in sync with user input on every keystroke by default (or on blur/change if you use the .lazy modifier).
Cricket analogy: Holding all scorecard fields in one reactive() object — batsman, runs, overs, wickets — rather than separate refs keeps the whole innings grouped and trivial to submit as one record, with each field bound via v-model="scoreState.runs" and so on.
Deriving Validity With Computed Properties
Rather than manually calling a validate() function on every keystroke, express each field's validity as a computed() that derives from the field's current value — this keeps validation automatically in sync with the data and cache-friendly. The form's overall isValid can then be a computed that combines every individual field computed, so the submit button's disabled state and any 'you have errors' banner all stay consistent for free.
Cricket analogy: Instead of manually rechecking on every ball, express a batsman's not-out status as a computed() derived from wickets fallen; the innings' overall isValid computed then combines every field's computed, so the declaration button's state stays consistent automatically.
<script setup>
import { reactive, computed, ref } from 'vue'
const form = reactive({ email: '', password: '' })
const touched = reactive({ email: false, password: false })
const emailError = computed(() => {
if (!form.email) return 'Email is required'
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) return 'Enter a valid email'
return ''
})
const passwordError = computed(() => {
if (form.password.length < 8) return 'Password must be at least 8 characters'
return ''
})
const isFormValid = computed(() => !emailError.value && !passwordError.value)
function handleSubmit() {
touched.email = true
touched.password = true
if (!isFormValid.value) return
// submit form.email / form.password to the API
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.email" @blur="touched.email = true" type="email" />
<p v-if="touched.email && emailError">{{ emailError }}</p>
<input v-model="form.password" @blur="touched.password = true" type="password" />
<p v-if="touched.password && passwordError">{{ passwordError }}</p>
<button type="submit" :disabled="!isFormValid">Sign Up</button>
</form>
</template>Timing Error Messages With 'Touched' State
Showing 'Email is required' the instant the page loads, before a user has typed anything, feels aggressive and unhelpful. The common fix is a parallel touched flag per field, set to true on the field's blur event (or on first submit attempt), and templates only render the error when both the field is invalid AND has been touched. This gives users room to type without premature judgment while still catching errors before submission.
Cricket analogy: Flashing 'Invalid declaration' the moment the scorecard opens, before anyone's entered runs, feels aggressive; a touched flag set true on the field's blur (or first submit attempt) means the error only shows once the scorer has actually left that field.
For complex, multi-page, or deeply nested forms, dedicated libraries like VeeValidate or vuelidate provide schema-based validation (often paired with a schema library like Yup or Zod), built-in touched/dirty tracking, and integration with UI component libraries — worth adopting once hand-rolled computed validators start feeling repetitive across many forms.
A subtle gotcha: if form is a reactive() object and you destructure a field out of it (const { email } = form), you lose the reactive connection to that property, just as with any reactive object. Always access fields via form.email in the template or wrap destructured fields with toRefs(form) if you need standalone refs.
- Group related form fields in a single
reactive()object bound viav-modelon each input. - Express per-field validity as
computed()properties so validation stays automatically in sync with input. - Combine per-field computeds into an overall
isFormValidcomputed to drive the submit button and summary banners. - Use a
touchedflag per field (set on blur or submit) to avoid showing errors before the user has interacted with a field. - For complex forms, schema-based libraries like VeeValidate reduce repetitive hand-rolled validation logic.
- Destructuring fields out of a reactive() form object loses reactivity — access via the object or use toRefs().
Practice what you learned
1. Why is expressing field validity as a `computed()` preferable to manually calling a validate function on every keystroke?
2. What is the purpose of a per-field 'touched' flag in a form?
3. What happens if you destructure a field from a `reactive()` form object, e.g. `const { email } = form`?
4. Which modifier changes v-model to sync on the input's change/blur event instead of every keystroke?
5. Why might a team adopt a library like VeeValidate instead of hand-rolled computed validators?
Was this page helpful?
You May Also Like
Form Input Bindings with v-model
Details how v-model implements two-way binding on form elements and components, what it compiles down to, and its modifiers and pitfalls.
Computed Properties
How Vue's computed() function derives cached, reactive values from other state, and why it's preferred over methods for derived data in templates.
reactive() and ref()
A deep dive into Vue 3's two core reactivity primitives — ref() for any value type and reactive() for objects — and when to use each.
Handling Loading and Error States
Explore patterns for modeling async status explicitly in Vue components so templates can reliably render loading, error, empty, and success UI.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics