Skip to content

Validation Rules

15.2.1. Client-Side Validation Strategy

Client-side validation is divided into two strategies: real-time validation and submit-time validation.

StrategyTrigger TimingApplicable Targets
Real-time validation@blur or watchEmail format, password strength, required fields
Submit-time validation@submit.preventFull form batch validation, cross-field validation
  • For better user experience, real-time validation should be the default strategy.
  • Validations with inter-field dependencies (e.g., password confirmation) should be combined with submit-time validation.
  • Real-time validation using the @input event must not be used as it causes excessive invocations. The @blur event must be used instead.

15.2.2. Required / Format / Range Validation

Validation types are classified into the following three categories, and validation functions must be written for each type.

TypeDescriptionExamples
Required validationChecks for value presenceEmpty string, null, undefined checks
Format validationChecks for pattern matchEmail, phone number, URL formats
Range validationChecks for size/length limitsMinimum 8 characters, maximum 100 characters, range 1-999
typescript
// src/utils/validators.ts
export function required(value: string): string | null {
  return value.trim() === '' ? 'This field is required.' : null
}

export function email(value: string): string | null {
  const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return pattern.test(value) ? null : 'Please enter a valid email address.'
}

export function minLength(min: number) {
  return (value: string): string | null => {
    return value.length < min ? `Must be at least ${min} characters.` : null
  }
}

export function maxLength(max: number) {
  return (value: string): string | null => {
    return value.length > max ? `Must not exceed ${max} characters.` : null
  }
}
  • Validation functions must return an error message string when invalid, or null when valid.
  • Range validations must be written as higher-order functions to inject threshold values.

15.2.3. Custom Rules

Reusable validation logic must be separated into composables.

typescript
// src/composables/useFormValidation.ts
import { ref, type Ref } from 'vue'

type Validator = (value: string) => string | null

export function useFormValidation<T extends Record<string, string>>(
  form: Ref<T>,
  rules: Partial<Record<keyof T, Validator[]>>
) {
  const errors = ref<Partial<Record<keyof T, string>>>({})

  function validateField(field: keyof T) {
    const fieldRules = rules[field]
    if (!fieldRules) return

    for (const rule of fieldRules) {
      const error = rule(form.value[field])
      if (error) {
        errors.value[field] = error
        return
      }
    }
    delete errors.value[field]
  }

  function validateAll(): boolean {
    for (const field of Object.keys(rules) as (keyof T)[]) {
      validateField(field)
    }
    return Object.keys(errors.value).length === 0
  }

  return { errors, validateField, validateAll }
}

Async validations (e.g., duplicate checks) must be handled with separate async validation functions.

typescript
async function checkDuplicateEmail(email: string): Promise<string | null> {
  const { isDuplicate } = await api.get(`/users/check-email?email=${email}`)
  return isDuplicate ? 'This email is already in use.' : null
}
  • Async validations must only be executed at the @blur timing.
  • Since network requests are involved, applying debounce is recommended.

15.2.4. Error Message Display

Validation errors must be displayed inline below the corresponding field.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useFormValidation } from '@/composables/useFormValidation'
import { required, email, minLength } from '@/utils/validators'

const form = ref({ email: '', password: '' })

const { errors, validateField, validateAll } = useFormValidation(form, {
  email: [required, email],
  password: [required, minLength(8)],
})
</script>

<template>
  <form @submit.prevent="validateAll() && handleSubmit()">
    <div :class="{ 'has-error': errors.email }">
      <input
        v-model="form.email"
        type="email"
        @blur="validateField('email')"
      />
      <p v-if="errors.email" class="error-message">{{ errors.email }}</p>
    </div>
    <div :class="{ 'has-error': errors.password }">
      <input
        v-model="form.password"
        type="password"
        @blur="validateField('password')"
      />
      <p v-if="errors.password" class="error-message">{{ errors.password }}</p>
    </div>
  </form>
</template>
  • Error messages must be conditionally rendered using v-if.
  • Fields in an error state must have the has-error class added to provide visual feedback.
  • Error messages must be written in a way that users can understand, with specific guidance included.

TIENIPIA QUALIFIED STANDARD