11 - Building Form Validation System
Duration: 15 minutes
Video Info
- Title: Building Form Validation System
- Series: Practical
- Level: Intermediate
- Prerequisites: Vue form basics, Composables
Chapters
- Requirements Analysis (2 min)
- Form Structure Setup (3 min)
- Real-time Validation (4 min)
- Submit Handling (3 min)
- Optimization (3 min)
Detailed Script
Opening (0:00-0:15)
Today we build a complete form validation system using Directix through a real-world example.
Chapter 1: Requirements (0:15-2:15)
Visual: Form prototype
We'll create a user registration form with:
- Name (required, capitalize first letter)
- Email (required, format validation)
- Phone (formatted display, validation)
- Password (strength validation)
- Confirm Password (match validation)
- Submit button (debounce, throttle)
Directives used:
v-trim- Trim spacesv-capitalcase- Capitalize first letterv-lowercase- Lowercase emailv-mask- Phone formattingv-debounce- Real-time validation debouncev-throttle- Submit throttlev-focus- Error field focus
Chapter 2: Form Structure (2:15-5:15)
Visual: VS Code demo
vue
<template>
<form @submit.prevent="handleSubmit" class="register-form">
<!-- Name -->
<div class="form-group">
<label>Name</label>
<input
v-model="form.name"
v-trim
v-capitalcase
v-debounce:300="validateName"
:class="{ error: errors.name }"
placeholder="Enter name"
/>
<span v-if="errors.name" class="error-msg">{{ errors.name }}</span>
</div>
<!-- Email -->
<div class="form-group">
<label>Email</label>
<input
v-model="form.email"
v-trim
v-lowercase
v-debounce:300="validateEmail"
type="email"
:class="{ error: errors.email }"
placeholder="Enter email"
/>
<span v-if="errors.email" class="error-msg">{{ errors.email }}</span>
</div>
<!-- Phone -->
<div class="form-group">
<label>Phone</label>
<input
v-model="form.phone"
v-mask="'### #### ####'"
v-debounce:300="validatePhone"
:class="{ error: errors.phone }"
placeholder="Enter phone"
/>
<span v-if="errors.phone" class="error-msg">{{ errors.phone }}</span>
</div>
<!-- Password -->
<div class="form-group">
<label>Password</label>
<input
v-model="form.password"
v-debounce:300="validatePassword"
type="password"
:class="{ error: errors.password }"
placeholder="Enter password"
/>
<div class="password-strength">
<div :class="['bar', strength]"></div>
<span>{{ strengthText }}</span>
</div>
</div>
<!-- Confirm Password -->
<div class="form-group">
<label>Confirm Password</label>
<input
v-model="form.confirmPassword"
v-debounce:300="validateConfirmPassword"
type="password"
:class="{ error: errors.confirmPassword }"
placeholder="Re-enter password"
/>
<span v-if="errors.confirmPassword" class="error-msg">
{{ errors.confirmPassword }}
</span>
</div>
<!-- Submit Button -->
<button
type="submit"
v-throttle:1000="handleSubmit"
:disabled="!isFormValid || submitting"
>
{{ submitting ? 'Submitting...' : 'Register' }}
</button>
</form>
</template>Chapter 3: Real-time Validation (5:15-9:15)
Visual: Validation logic
vue
<script setup>
import { ref, reactive, computed } from 'vue'
import { useDebounce } from 'directix'
// Form data
const form = reactive({
name: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
})
// Error messages
const errors = reactive({
name: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
})
// Validation rules
const validators = {
name: (value) => {
if (!value) return 'Please enter name'
if (value.length < 2) return 'Name must be at least 2 characters'
return ''
},
email: (value) => {
if (!value) return 'Please enter email'
if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(value)) return 'Invalid email format'
return ''
},
phone: (value) => {
const cleaned = value.replace(/\s/g, '')
if (!cleaned) return 'Please enter phone'
if (!/^1[3-9]\d{9}$/.test(cleaned)) return 'Invalid phone format'
return ''
},
password: (value) => {
if (!value) return 'Please enter password'
if (value.length < 8) return 'Password must be at least 8 characters'
if (!/[A-Z]/.test(value)) return 'Must contain uppercase letter'
if (!/[0-9]/.test(value)) return 'Must contain number'
return ''
},
confirmPassword: (value) => {
if (!value) return 'Please confirm password'
if (value !== form.password) return 'Passwords do not match'
return ''
}
}
// Validation functions
const validateName = () => { errors.name = validators.name(form.name) }
const validateEmail = () => { errors.email = validators.email(form.email) }
const validatePhone = () => { errors.phone = validators.phone(form.phone) }
const validatePassword = () => {
errors.password = validators.password(form.password)
updateStrength()
}
const validateConfirmPassword = () => {
errors.confirmPassword = validators.confirmPassword(form.confirmPassword)
}
// Password strength
const strength = ref('weak')
const strengthText = computed(() => {
const map = { weak: 'Weak', medium: 'Medium', strong: 'Strong' }
return map[strength.value]
})
const updateStrength = () => {
const pwd = form.password
let score = 0
if (pwd.length >= 8) score++
if (/[A-Z]/.test(pwd)) score++
if (/[a-z]/.test(pwd)) score++
if (/[0-9]/.test(pwd)) score++
if (/[^A-Za-z0-9]/.test(pwd)) score++
strength.value = score <= 2 ? 'weak' : score <= 4 ? 'medium' : 'strong'
}
// Form validity
const isFormValid = computed(() => {
return Object.values(errors).every(e => e === '')
&& Object.values(form).every(v => v !== '')
})
const submitting = ref(false)
</script>Chapter 4: Submit Handling (9:15-12:15)
Visual: Submit logic
vue
<script setup>
// ... previous code
// Validate all
const validateAll = () => {
validateName()
validateEmail()
validatePhone()
validatePassword()
validateConfirmPassword()
return isFormValid.value
}
// Focus first error field
const focusFirstError = () => {
const firstError = Object.keys(errors).find(key => errors[key])
if (firstError) {
const input = document.querySelector(`[v-model="form.${firstError}"]`)
if (input) input.focus()
}
}
// Submit handler
const handleSubmit = async () => {
if (!validateAll()) {
focusFirstError()
return
}
submitting.value = true
try {
// Clean phone spaces
const data = {
...form,
phone: form.phone.replace(/\s/g, '')
}
await api.register(data)
showToast('Registration successful!')
router.push('/login')
} catch (error) {
showToast(error.message || 'Registration failed, please try again')
} finally {
submitting.value = false
}
}
</script>Chapter 5: Optimization (12:15-15:00)
Visual: Optimized code
1. Optimize with useDebounce:
vue
<script setup>
import { useDebounce } from 'directix'
// Convert validation to debounced functions
const debouncedValidateName = useDebounce(validateName, 300)
</script>
<template>
<input @input="debouncedValidateName" />
</template>2. Add Auto-save:
vue
<script setup>
import { useDebounce } from 'directix'
// Auto-save draft
const { debouncedValue } = useDebounce(form, 1000)
watch(debouncedValue, () => {
localStorage.setItem('register-draft', JSON.stringify(form))
})
</script>3. Add Copy Feature:
vue
<template>
<div class="form-group">
<label>Invite Code</label>
<div class="input-group">
<input v-model="inviteCode" readonly />
<button v-copy="inviteCode">Copy</button>
</div>
</div>
</template>Summary
Today we built a complete form validation system:
- Combined multiple directives for functionality
- Real-time validation with error messages
- Password strength detection
- Submit throttling to prevent duplicates
- User experience optimization
Next video covers infinite scroll lists.
Exercises
- Add verification code input with v-mask formatting
- Implement form data local storage, restore after refresh
- Add submit success animation