car_mms/app/hooks/useFormValidation.ts
2025-09-11 14:22:27 +03:00

219 lines
5.9 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import { z } from 'zod';
interface UseFormValidationOptions<T> {
schema: z.ZodSchema<T>;
initialValues: Partial<T>;
validateOnChange?: boolean;
validateOnBlur?: boolean;
}
interface FormState<T> {
values: Partial<T>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isValid: boolean;
isSubmitting: boolean;
}
export function useFormValidation<T extends Record<string, any>>({
schema,
initialValues,
validateOnChange = true,
validateOnBlur = true,
}: UseFormValidationOptions<T>) {
const [state, setState] = useState<FormState<T>>({
values: initialValues,
errors: {},
touched: {},
isValid: false,
isSubmitting: false,
});
// Validate a single field
const validateField = useCallback((name: keyof T, value: any): string | null => {
try {
// Get the field schema
const fieldSchema = (schema as any).shape[name];
if (fieldSchema) {
fieldSchema.parse(value);
}
return null;
} catch (error) {
if (error instanceof z.ZodError) {
return error.errors[0]?.message || null;
}
return null;
}
}, [schema]);
// Validate all fields
const validateForm = useCallback((values: Partial<T>): Record<string, string> => {
try {
schema.parse(values);
return {};
} catch (error) {
if (error instanceof z.ZodError) {
const errors: Record<string, string> = {};
error.errors.forEach((err) => {
if (err.path.length > 0) {
errors[err.path[0] as string] = err.message;
}
});
return errors;
}
return {};
}
}, [schema]);
// Set field value
const setValue = useCallback((name: keyof T, value: any) => {
setState(prev => {
const newValues = { ...prev.values, [name]: value };
const fieldError = validateOnChange ? validateField(name, value) : null;
const newErrors = { ...prev.errors };
if (fieldError) {
newErrors[name as string] = fieldError;
} else {
delete newErrors[name as string];
}
const allErrors = validateOnChange ? validateForm(newValues) : newErrors;
const isValid = Object.keys(allErrors).length === 0;
return {
...prev,
values: newValues,
errors: allErrors,
isValid,
};
});
}, [validateField, validateForm, validateOnChange]);
// Set field as touched
const setTouched = useCallback((name: keyof T, touched = true) => {
setState(prev => {
const newTouched = { ...prev.touched, [name]: touched };
let newErrors = { ...prev.errors };
if (touched && validateOnBlur) {
const fieldError = validateField(name, prev.values[name]);
if (fieldError) {
newErrors[name as string] = fieldError;
}
}
return {
...prev,
touched: newTouched,
errors: newErrors,
};
});
}, [validateField, validateOnBlur]);
// Set multiple values
const setValues = useCallback((values: Partial<T>) => {
setState(prev => {
const newValues = { ...prev.values, ...values };
const errors = validateForm(newValues);
const isValid = Object.keys(errors).length === 0;
return {
...prev,
values: newValues,
errors,
isValid,
};
});
}, [validateForm]);
// Reset form
const reset = useCallback((newInitialValues?: Partial<T>) => {
const resetValues = newInitialValues || initialValues;
setState({
values: resetValues,
errors: {},
touched: {},
isValid: false,
isSubmitting: false,
});
}, [initialValues]);
// Set submitting state
const setSubmitting = useCallback((isSubmitting: boolean) => {
setState(prev => ({ ...prev, isSubmitting }));
}, []);
// Validate entire form and return validation result
const validate = useCallback(() => {
const errors = validateForm(state.values);
const isValid = Object.keys(errors).length === 0;
setState(prev => ({
...prev,
errors,
isValid,
touched: Object.keys(prev.values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>),
}));
return { isValid, errors };
}, [state.values, validateForm]);
// Get field props for easy integration with form components
const getFieldProps = useCallback((name: keyof T) => {
return {
name: name as string,
value: state.values[name] || '',
error: state.touched[name as string] ? state.errors[name as string] : undefined,
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setValue(name, e.target.value);
},
onBlur: () => {
setTouched(name, true);
},
};
}, [state.values, state.errors, state.touched, setValue, setTouched]);
// Get field error
const getFieldError = useCallback((name: keyof T): string | undefined => {
return state.touched[name as string] ? state.errors[name as string] : undefined;
}, [state.errors, state.touched]);
// Check if field has error
const hasFieldError = useCallback((name: keyof T): boolean => {
return !!(state.touched[name as string] && state.errors[name as string]);
}, [state.errors, state.touched]);
// Update validation when schema or initial values change
useEffect(() => {
const errors = validateForm(state.values);
const isValid = Object.keys(errors).length === 0;
setState(prev => ({
...prev,
errors,
isValid,
}));
}, [schema, validateForm]);
return {
values: state.values,
errors: state.errors,
touched: state.touched,
isValid: state.isValid,
isSubmitting: state.isSubmitting,
setValue,
setValues,
setTouched,
setSubmitting,
reset,
validate,
getFieldProps,
getFieldError,
hasFieldError,
};
}