Validation Rules
15.2.1. Client-Side Validation Strategy
Client-side validation is divided into two strategies: real-time validation and submit-time validation.
| Strategy | Trigger Timing | Applicable Targets |
|---|---|---|
| Real-time validation | @blur or watch | Email format, password strength, required fields |
| Submit-time validation | @submit.prevent | Full 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
@inputevent must not be used as it causes excessive invocations. The@blurevent 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.
| Type | Description | Examples |
|---|---|---|
| Required validation | Checks for value presence | Empty string, null, undefined checks |
| Format validation | Checks for pattern match | Email, phone number, URL formats |
| Range validation | Checks for size/length limits | Minimum 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
nullwhen 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
@blurtiming. - 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-errorclass added to provide visual feedback. - Error messages must be written in a way that users can understand, with specific guidance included.