97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
|
|
import { useState, useCallback, useMemo } from "react";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Hook for managing form state with validation and dirty checking.
|
||
|
|
* Handles common form patterns like tracking changes, validation, and submission.
|
||
|
|
*
|
||
|
|
* @param initialData - Initial form data
|
||
|
|
* @param validator - Validation function that returns field errors
|
||
|
|
* @param onSubmit - Submit handler function
|
||
|
|
* @returns Form state and handlers
|
||
|
|
*/
|
||
|
|
export function useForm<
|
||
|
|
TData extends Record<string, unknown>,
|
||
|
|
TErrors extends Record<string, string | undefined>,
|
||
|
|
>(
|
||
|
|
initialData: TData,
|
||
|
|
validator: (data: TData) => TErrors,
|
||
|
|
onSubmit: (data: TData) => Promise<void>
|
||
|
|
): {
|
||
|
|
formData: TData;
|
||
|
|
setFormData: React.Dispatch<React.SetStateAction<TData>>;
|
||
|
|
errors: TErrors;
|
||
|
|
setErrors: React.Dispatch<React.SetStateAction<TErrors>>;
|
||
|
|
isDirty: boolean;
|
||
|
|
isValid: boolean;
|
||
|
|
isSubmitting: boolean;
|
||
|
|
handleChange: (field: keyof TData) => (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||
|
|
handleSubmit: (e: React.FormEvent) => Promise<void>;
|
||
|
|
reset: () => void;
|
||
|
|
} {
|
||
|
|
const [formData, setFormData] = useState<TData>(initialData);
|
||
|
|
const [originalData] = useState<TData>(initialData);
|
||
|
|
const [errors, setErrors] = useState<TErrors>({} as TErrors);
|
||
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
|
|
|
||
|
|
const isDirty = useMemo(() => {
|
||
|
|
return JSON.stringify(formData) !== JSON.stringify(originalData);
|
||
|
|
}, [formData, originalData]);
|
||
|
|
|
||
|
|
const isValid = useMemo(() => {
|
||
|
|
return Object.keys(errors).length === 0;
|
||
|
|
}, [errors]);
|
||
|
|
|
||
|
|
const handleChange = useCallback(
|
||
|
|
(field: keyof TData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
|
||
|
|
// Clear error for this field when user starts typing
|
||
|
|
setErrors((prev) => {
|
||
|
|
const newErrors = { ...prev };
|
||
|
|
delete newErrors[field as keyof TErrors];
|
||
|
|
return newErrors;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleSubmit = useCallback(
|
||
|
|
async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
// Validate all fields
|
||
|
|
const validationErrors = validator(formData);
|
||
|
|
setErrors(validationErrors);
|
||
|
|
|
||
|
|
if (Object.keys(validationErrors).length > 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsSubmitting(true);
|
||
|
|
try {
|
||
|
|
await onSubmit(formData);
|
||
|
|
} finally {
|
||
|
|
setIsSubmitting(false);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[formData, validator, onSubmit]
|
||
|
|
);
|
||
|
|
|
||
|
|
const reset = useCallback(() => {
|
||
|
|
setFormData(originalData);
|
||
|
|
setErrors({} as TErrors);
|
||
|
|
}, [originalData]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
formData,
|
||
|
|
setFormData,
|
||
|
|
errors,
|
||
|
|
setErrors,
|
||
|
|
isDirty,
|
||
|
|
isValid,
|
||
|
|
isSubmitting,
|
||
|
|
handleChange,
|
||
|
|
handleSubmit,
|
||
|
|
reset,
|
||
|
|
};
|
||
|
|
}
|