219 lines
5.9 KiB
TypeScript
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,
|
|
};
|
|
} |