import { useState, useCallback, useMemo } from 'react'; type ValidationRule = (value: T, formData?: Record) => string | undefined; type ValidationSchema> = { [K in keyof T]?: ValidationRule[]; }; interface UseFormValidationResult> { /** Current validation errors */ errors: Partial>; /** Whether the form is valid (no errors) */ isValid: boolean; /** Validate a single field */ validateField: (field: keyof T, value: T[keyof T]) => string | undefined; /** Validate all fields */ validateForm: (data: T) => boolean; /** Set a specific error */ setError: (field: keyof T, error: string) => void; /** Clear a specific error */ clearError: (field: keyof T) => void; /** Clear all errors */ clearAllErrors: () => void; /** Get error for a field */ getError: (field: keyof T) => string | undefined; /** Check if a field has an error */ hasError: (field: keyof T) => boolean; } /** * Form validation hook with schema-based validation * * @example * const schema = { * email: [required('Email is required'), email('Invalid email')], * password: [required('Password is required'), minLength(8, 'Password must be at least 8 characters')], * }; * * const { errors, validateForm, validateField, isValid } = useFormValidation(schema); * * const handleSubmit = () => { * if (validateForm(formData)) { * // Submit form * } * }; */ export function useFormValidation>( schema: ValidationSchema ): UseFormValidationResult { const [errors, setErrors] = useState>>({}); const validateField = useCallback( (field: keyof T, value: T[keyof T], formData?: T): string | undefined => { const rules = schema[field]; if (!rules) return undefined; for (const rule of rules) { const error = rule(value, formData as Record); if (error) return error; } return undefined; }, [schema] ); const validateForm = useCallback( (data: T): boolean => { const newErrors: Partial> = {}; let isValid = true; for (const field of Object.keys(schema) as (keyof T)[]) { const error = validateField(field, data[field], data); if (error) { newErrors[field] = error; isValid = false; } } setErrors(newErrors); return isValid; }, [schema, validateField] ); const setError = useCallback((field: keyof T, error: string) => { setErrors((prev) => ({ ...prev, [field]: error })); }, []); const clearError = useCallback((field: keyof T) => { setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[field]; return newErrors; }); }, []); const clearAllErrors = useCallback(() => { setErrors({}); }, []); const getError = useCallback( (field: keyof T): string | undefined => errors[field], [errors] ); const hasError = useCallback( (field: keyof T): boolean => !!errors[field], [errors] ); const isValid = useMemo(() => Object.keys(errors).length === 0, [errors]); return { errors, isValid, validateField, validateForm, setError, clearError, clearAllErrors, getError, hasError, }; } // ============= Built-in validation rules ============= /** * Required field validation */ export const required = (message = 'This field is required'): ValidationRule => { return (value) => { if (value === undefined || value === null || value === '') { return message; } if (Array.isArray(value) && value.length === 0) { return message; } return undefined; }; }; /** * Email validation */ export const email = (message = 'Invalid email address'): ValidationRule => { return (value) => { if (!value) return undefined; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(value) ? undefined : message; }; }; /** * Minimum length validation */ export const minLength = (min: number, message?: string): ValidationRule => { return (value) => { if (!value) return undefined; return value.length >= min ? undefined : message || `Must be at least ${min} characters`; }; }; /** * Maximum length validation */ export const maxLength = (max: number, message?: string): ValidationRule => { return (value) => { if (!value) return undefined; return value.length <= max ? undefined : message || `Must be at most ${max} characters`; }; }; /** * Minimum value validation (for numbers) */ export const minValue = (min: number, message?: string): ValidationRule => { return (value) => { if (value === undefined || value === null) return undefined; return value >= min ? undefined : message || `Must be at least ${min}`; }; }; /** * Maximum value validation (for numbers) */ export const maxValue = (max: number, message?: string): ValidationRule => { return (value) => { if (value === undefined || value === null) return undefined; return value <= max ? undefined : message || `Must be at most ${max}`; }; }; /** * Pattern/regex validation */ export const pattern = (regex: RegExp, message = 'Invalid format'): ValidationRule => { return (value) => { if (!value) return undefined; return regex.test(value) ? undefined : message; }; }; /** * URL validation */ export const url = (message = 'Invalid URL'): ValidationRule => { return (value) => { if (!value) return undefined; try { new URL(value); return undefined; } catch { return message; } }; }; /** * Match another field (e.g., confirm password) */ export const matches = >( fieldName: keyof T, message = 'Fields must match' ): ValidationRule => { return (value, formData) => { if (!formData) return undefined; return value === formData[fieldName as string] ? undefined : message; }; }; /** * Phone number validation (basic) */ export const phone = (message = 'Invalid phone number'): ValidationRule => { return (value) => { if (!value) return undefined; const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/; return phoneRegex.test(value) ? undefined : message; }; }; export default useFormValidation;