Files
klz-cables.com/components/forms/hooks/useFormValidation.ts
2025-12-29 18:18:48 +01:00

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,
};
}