Skip to content

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

  1. Requirements Analysis (2 min)
  2. Form Structure Setup (3 min)
  3. Real-time Validation (4 min)
  4. Submit Handling (3 min)
  5. 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 spaces
  • v-capitalcase - Capitalize first letter
  • v-lowercase - Lowercase email
  • v-mask - Phone formatting
  • v-debounce - Real-time validation debounce
  • v-throttle - Submit throttle
  • v-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

  1. Add verification code input with v-mask formatting
  2. Implement form data local storage, restore after refresh
  3. Add submit success animation

Code

GitHub - examples/vue3

Released under the MIT License.