264 lines
6.2 KiB
TypeScript
264 lines
6.2 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
/**
|
|
* Form Validation Hooks
|
|
* Provides validation logic and utilities for form components
|
|
*/
|
|
|
|
export interface ValidationRule {
|
|
value: any;
|
|
message: string;
|
|
}
|
|
|
|
export interface ValidationRules {
|
|
required?: boolean | string;
|
|
minLength?: ValidationRule;
|
|
maxLength?: ValidationRule;
|
|
pattern?: ValidationRule;
|
|
min?: ValidationRule;
|
|
max?: ValidationRule;
|
|
email?: boolean | string;
|
|
url?: boolean | string;
|
|
number?: boolean | string;
|
|
custom?: (value: any) => string | null;
|
|
}
|
|
|
|
export interface ValidationError {
|
|
field: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface FormErrors {
|
|
[key: string]: string[];
|
|
}
|
|
|
|
/**
|
|
* Validates a single field value against validation rules
|
|
*/
|
|
export function validateField(
|
|
value: any,
|
|
rules: ValidationRules,
|
|
fieldName: string
|
|
): string[] {
|
|
const errors: string[] = [];
|
|
|
|
// Required validation
|
|
if (rules.required) {
|
|
const requiredMessage = typeof rules.required === 'string'
|
|
? rules.required
|
|
: `${fieldName} is required`;
|
|
|
|
if (value === null || value === undefined || value === '') {
|
|
errors.push(requiredMessage);
|
|
}
|
|
}
|
|
|
|
// Only validate other rules if there's a value (unless required)
|
|
if (value === null || value === undefined || value === '') {
|
|
return errors;
|
|
}
|
|
|
|
// Min length validation
|
|
if (rules.minLength) {
|
|
const min = rules.minLength.value;
|
|
const message = rules.minLength.message || `${fieldName} must be at least ${min} characters`;
|
|
|
|
if (typeof value === 'string' && value.length < min) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Max length validation
|
|
if (rules.maxLength) {
|
|
const max = rules.maxLength.value;
|
|
const message = rules.maxLength.message || `${fieldName} must be at most ${max} characters`;
|
|
|
|
if (typeof value === 'string' && value.length > max) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Pattern validation
|
|
if (rules.pattern) {
|
|
const pattern = rules.pattern.value;
|
|
const message = rules.pattern.message || `${fieldName} format is invalid`;
|
|
|
|
if (typeof value === 'string' && !pattern.test(value)) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Min value validation
|
|
if (rules.min) {
|
|
const min = rules.min.value;
|
|
const message = rules.min.message || `${fieldName} must be at least ${min}`;
|
|
|
|
if (typeof value === 'number' && value < min) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Max value validation
|
|
if (rules.max) {
|
|
const max = rules.max.value;
|
|
const message = rules.max.message || `${fieldName} must be at most ${max}`;
|
|
|
|
if (typeof value === 'number' && value > max) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Email validation
|
|
if (rules.email) {
|
|
const message = typeof rules.email === 'string'
|
|
? rules.email
|
|
: 'Please enter a valid email address';
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (typeof value === 'string' && !emailRegex.test(value)) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// URL validation
|
|
if (rules.url) {
|
|
const message = typeof rules.url === 'string'
|
|
? rules.url
|
|
: 'Please enter a valid URL';
|
|
|
|
try {
|
|
new URL(value);
|
|
} catch {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Number validation
|
|
if (rules.number) {
|
|
const message = typeof rules.number === 'string'
|
|
? rules.number
|
|
: 'Please enter a valid number';
|
|
|
|
if (isNaN(Number(value))) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
|
|
// Custom validation
|
|
if (rules.custom) {
|
|
const customError = rules.custom(value);
|
|
if (customError) {
|
|
errors.push(customError);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Validates an entire form against validation rules
|
|
*/
|
|
export function validateForm<T extends Record<string, any>>(
|
|
values: T,
|
|
validationRules: Record<keyof T, ValidationRules>
|
|
): { isValid: boolean; errors: FormErrors } {
|
|
const errors: FormErrors = {};
|
|
let isValid = true;
|
|
|
|
Object.keys(validationRules).forEach((fieldName) => {
|
|
const fieldRules = validationRules[fieldName as keyof T];
|
|
const fieldValue = values[fieldName];
|
|
const fieldErrors = validateField(fieldValue, fieldRules, fieldName);
|
|
|
|
if (fieldErrors.length > 0) {
|
|
errors[fieldName] = fieldErrors;
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
return { isValid, errors };
|
|
}
|
|
|
|
/**
|
|
* Hook for form validation
|
|
*/
|
|
export function useFormValidation<T extends Record<string, any>>(
|
|
initialValues: T,
|
|
validationRules: Record<keyof T, ValidationRules>
|
|
) {
|
|
const [values, setValues] = useState<T>(initialValues);
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
const [touched, setTouched] = useState<Record<keyof T, boolean>>(
|
|
Object.keys(initialValues).reduce((acc, key) => {
|
|
acc[key as keyof T] = false;
|
|
return acc;
|
|
}, {} as Record<keyof T, boolean>)
|
|
);
|
|
const [isValid, setIsValid] = useState(false);
|
|
|
|
const validate = useCallback(() => {
|
|
const validation = validateForm(values, validationRules);
|
|
setErrors(validation.errors);
|
|
setIsValid(validation.isValid);
|
|
return validation;
|
|
}, [values, validationRules]);
|
|
|
|
useEffect(() => {
|
|
validate();
|
|
}, [validate]);
|
|
|
|
const setFieldValue = (field: keyof T, value: any) => {
|
|
setValues((prev) => ({ ...prev, [field]: value }));
|
|
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
};
|
|
|
|
const setFieldError = (field: keyof T, error: string) => {
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
[field]: [...(prev[field as string] || []), error],
|
|
}));
|
|
};
|
|
|
|
const clearFieldError = (field: keyof T) => {
|
|
setErrors((prev) => {
|
|
const newErrors = { ...prev };
|
|
delete newErrors[field as string];
|
|
return newErrors;
|
|
});
|
|
};
|
|
|
|
const reset = () => {
|
|
setValues(initialValues);
|
|
setErrors({});
|
|
setTouched(
|
|
Object.keys(initialValues).reduce((acc, key) => {
|
|
acc[key as keyof T] = false;
|
|
return acc;
|
|
}, {} as Record<keyof T, boolean>)
|
|
);
|
|
setIsValid(false);
|
|
};
|
|
|
|
const setAllTouched = () => {
|
|
setTouched(
|
|
Object.keys(values).reduce((acc, key) => {
|
|
acc[key as keyof T] = true;
|
|
return acc;
|
|
}, {} as Record<keyof T, boolean>)
|
|
);
|
|
};
|
|
|
|
return {
|
|
values,
|
|
errors,
|
|
touched,
|
|
isValid,
|
|
setFieldValue,
|
|
setFieldError,
|
|
clearFieldError,
|
|
validate,
|
|
reset,
|
|
setAllTouched,
|
|
setValues,
|
|
};
|
|
} |