275 lines
7.3 KiB
TypeScript
275 lines
7.3 KiB
TypeScript
import { useState, useCallback, FormEvent } from 'react';
|
|
import { useFormValidation, ValidationRules, FormErrors } from './useFormValidation';
|
|
|
|
/**
|
|
* Hook for managing complete form state and submission
|
|
*/
|
|
|
|
export interface FormState<T extends Record<string, any>> {
|
|
values: T;
|
|
errors: FormErrors;
|
|
touched: Record<keyof T, boolean>;
|
|
isValid: boolean;
|
|
isSubmitting: boolean;
|
|
isSubmitted: boolean;
|
|
submitCount: number;
|
|
}
|
|
|
|
export interface FormOptions<T extends Record<string, any>> {
|
|
initialValues: T;
|
|
validationRules: Record<keyof T, ValidationRules>;
|
|
onSubmit: (values: T) => Promise<void> | void;
|
|
validateOnMount?: boolean;
|
|
}
|
|
|
|
export interface FormReturn<T extends Record<string, any>> extends FormState<T> {
|
|
setFieldValue: (field: keyof T, value: any) => void;
|
|
setFieldError: (field: keyof T, error: string) => void;
|
|
clearFieldError: (field: keyof T) => void;
|
|
handleChange: (field: keyof T, value: any) => void;
|
|
handleSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>;
|
|
reset: () => void;
|
|
setAllTouched: () => void;
|
|
setValues: (values: T) => void;
|
|
setErrors: (errors: FormErrors) => void;
|
|
setSubmitting: (isSubmitting: boolean) => void;
|
|
getFormProps: () => { onSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>; noValidate: boolean };
|
|
}
|
|
|
|
/**
|
|
* Hook for managing complete form state with validation and submission
|
|
*/
|
|
export function useForm<T extends Record<string, any>>(
|
|
options: FormOptions<T>
|
|
): FormReturn<T> {
|
|
const {
|
|
initialValues,
|
|
validationRules,
|
|
onSubmit,
|
|
validateOnMount = false,
|
|
} = options;
|
|
|
|
const {
|
|
values,
|
|
errors,
|
|
touched,
|
|
isValid,
|
|
setFieldValue: validationSetFieldValue,
|
|
setFieldError: validationSetFieldError,
|
|
clearFieldError: validationClearFieldError,
|
|
validate,
|
|
reset: validationReset,
|
|
setAllTouched: validationSetAllTouched,
|
|
setValues: validationSetValues,
|
|
} = useFormValidation<T>(initialValues, validationRules);
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
const [submitCount, setSubmitCount] = useState(0);
|
|
|
|
// Validate on mount if requested
|
|
// Note: This is handled by useFormValidation's useEffect
|
|
|
|
const setFieldValue = useCallback((field: keyof T, value: any) => {
|
|
validationSetFieldValue(field, value);
|
|
}, [validationSetFieldValue]);
|
|
|
|
const setFieldError = useCallback((field: keyof T, error: string) => {
|
|
validationSetFieldError(field, error);
|
|
}, [validationSetFieldError]);
|
|
|
|
const clearFieldError = useCallback((field: keyof T) => {
|
|
validationClearFieldError(field);
|
|
}, [validationClearFieldError]);
|
|
|
|
const handleChange = useCallback((field: keyof T, value: any) => {
|
|
setFieldValue(field, value);
|
|
}, [setFieldValue]);
|
|
|
|
const setErrors = useCallback((newErrors: FormErrors) => {
|
|
Object.entries(newErrors).forEach(([field, fieldErrors]) => {
|
|
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
fieldErrors.forEach((error) => {
|
|
setFieldError(field as keyof T, error);
|
|
});
|
|
}
|
|
});
|
|
}, [setFieldError]);
|
|
|
|
const setSubmitting = useCallback((state: boolean) => {
|
|
setIsSubmitting(state);
|
|
}, []);
|
|
|
|
const reset = useCallback(() => {
|
|
validationReset();
|
|
setIsSubmitting(false);
|
|
setIsSubmitted(false);
|
|
setSubmitCount(0);
|
|
}, [validationReset]);
|
|
|
|
const setAllTouched = useCallback(() => {
|
|
validationSetAllTouched();
|
|
}, [validationSetAllTouched]);
|
|
|
|
const setValues = useCallback((newValues: T) => {
|
|
validationSetValues(newValues);
|
|
}, [validationSetValues]);
|
|
|
|
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Increment submit count
|
|
setSubmitCount((prev) => prev + 1);
|
|
|
|
// Set all fields as touched to show validation errors
|
|
setAllTouched();
|
|
|
|
// Validate form
|
|
const validation = validate();
|
|
|
|
if (!validation.isValid) {
|
|
return;
|
|
}
|
|
|
|
// Start submission
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
// Call submit handler
|
|
await onSubmit(values);
|
|
setIsSubmitted(true);
|
|
} catch (error) {
|
|
// Handle submission error
|
|
console.error('Form submission error:', error);
|
|
|
|
// You can set a general error or handle specific error cases
|
|
if (error instanceof Error) {
|
|
setFieldError('submit' as keyof T, error.message);
|
|
} else {
|
|
setFieldError('submit' as keyof T, 'An error occurred during submission');
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [values, onSubmit, validate, setAllTouched, setFieldError]);
|
|
|
|
const getFormProps = useCallback(() => ({
|
|
onSubmit: handleSubmit,
|
|
noValidate: true,
|
|
}), [handleSubmit]);
|
|
|
|
return {
|
|
values,
|
|
errors,
|
|
touched,
|
|
isValid,
|
|
isSubmitting,
|
|
isSubmitted,
|
|
submitCount,
|
|
setFieldValue,
|
|
setFieldError,
|
|
clearFieldError,
|
|
handleChange,
|
|
handleSubmit,
|
|
reset,
|
|
setAllTouched,
|
|
setValues,
|
|
setErrors,
|
|
setSubmitting,
|
|
getFormProps,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook for managing form state with additional utilities
|
|
*/
|
|
export function useFormWithHelpers<T extends Record<string, any>>(
|
|
options: FormOptions<T>
|
|
) {
|
|
const form = useForm<T>(options);
|
|
|
|
const getFormProps = () => ({
|
|
onSubmit: form.handleSubmit,
|
|
noValidate: true, // We handle validation manually
|
|
});
|
|
|
|
const getSubmitButtonProps = () => ({
|
|
type: 'submit',
|
|
disabled: form.isSubmitting || !form.isValid,
|
|
loading: form.isSubmitting,
|
|
});
|
|
|
|
const getResetButtonProps = () => ({
|
|
type: 'button',
|
|
onClick: form.reset,
|
|
disabled: form.isSubmitting,
|
|
});
|
|
|
|
const getFieldProps = (field: keyof T) => ({
|
|
value: form.values[field] as any,
|
|
onChange: (e: any) => {
|
|
const target = e.target;
|
|
let value: any = target.value;
|
|
|
|
if (target.type === 'checkbox') {
|
|
value = target.checked;
|
|
} else if (target.type === 'number') {
|
|
value = target.value === '' ? '' : Number(target.value);
|
|
}
|
|
|
|
form.setFieldValue(field, value);
|
|
},
|
|
error: form.errors[field as string]?.[0],
|
|
touched: form.touched[field],
|
|
onBlur: () => {
|
|
// Mark as touched on blur if not already
|
|
if (!form.touched[field]) {
|
|
form.setAllTouched();
|
|
}
|
|
},
|
|
});
|
|
|
|
const hasFieldError = (field: keyof T): boolean => {
|
|
return !!form.errors[field as string]?.length && !!form.touched[field];
|
|
};
|
|
|
|
const getFieldError = (field: keyof T): string | null => {
|
|
const errors = form.errors[field as string];
|
|
return errors && errors.length > 0 ? errors[0] : null;
|
|
};
|
|
|
|
const clearFieldError = (field: keyof T) => {
|
|
form.clearFieldError(field);
|
|
};
|
|
|
|
const setFieldError = (field: keyof T, error: string) => {
|
|
form.setFieldError(field, error);
|
|
};
|
|
|
|
const isDirty = (): boolean => {
|
|
return Object.keys(form.values).some((key) => {
|
|
const currentValue = form.values[key as keyof T];
|
|
const initialValue = options.initialValues[key as keyof T];
|
|
return currentValue !== initialValue;
|
|
});
|
|
};
|
|
|
|
const canSubmit = (): boolean => {
|
|
return !form.isSubmitting && form.isValid && isDirty();
|
|
};
|
|
|
|
return {
|
|
...form,
|
|
getFormProps,
|
|
getSubmitButtonProps,
|
|
getResetButtonProps,
|
|
getFieldProps,
|
|
hasFieldError,
|
|
getFieldError,
|
|
clearFieldError,
|
|
setFieldError,
|
|
isDirty,
|
|
canSubmit,
|
|
};
|
|
} |