migration wip
This commit is contained in:
275
components/forms/hooks/useForm.ts
Normal file
275
components/forms/hooks/useForm.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
211
components/forms/hooks/useFormField.ts
Normal file
211
components/forms/hooks/useFormField.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useState, useCallback, ChangeEvent } from 'react';
|
||||
|
||||
/**
|
||||
* Hook for managing individual form field state
|
||||
*/
|
||||
|
||||
export interface FormFieldState<T> {
|
||||
value: T;
|
||||
error: string | null;
|
||||
touched: boolean;
|
||||
dirty: boolean;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
export interface FormFieldOptions<T> {
|
||||
initialValue?: T;
|
||||
validate?: (value: T) => string | null;
|
||||
transform?: (value: T) => T;
|
||||
}
|
||||
|
||||
export interface FormFieldReturn<T> {
|
||||
value: T;
|
||||
error: string | null;
|
||||
touched: boolean;
|
||||
dirty: boolean;
|
||||
isValid: boolean;
|
||||
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
||||
setValue: (value: T) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setTouched: (touched: boolean) => void;
|
||||
reset: () => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing individual form field state with validation
|
||||
*/
|
||||
export function useFormField<T = string>(
|
||||
options: FormFieldOptions<T> = {}
|
||||
): FormFieldReturn<T> {
|
||||
const {
|
||||
initialValue = '' as unknown as T,
|
||||
validate,
|
||||
transform,
|
||||
} = options;
|
||||
|
||||
const [state, setState] = useState<FormFieldState<T>>({
|
||||
value: initialValue,
|
||||
error: null,
|
||||
touched: false,
|
||||
dirty: false,
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
const validateValue = useCallback((value: T): string | null => {
|
||||
if (validate) {
|
||||
return validate(value);
|
||||
}
|
||||
return null;
|
||||
}, [validate]);
|
||||
|
||||
const updateState = useCallback((newState: Partial<FormFieldState<T>>) => {
|
||||
setState((prev) => {
|
||||
const updated = { ...prev, ...newState };
|
||||
|
||||
// Auto-validate if value changes and validation is provided
|
||||
if ('value' in newState && validate) {
|
||||
const error = validateValue(newState.value as T);
|
||||
updated.error = error;
|
||||
updated.isValid = !error;
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
}, [validate, validateValue]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
let value: any = e.target.value;
|
||||
|
||||
// Handle different input types
|
||||
if (e.target.type === 'checkbox') {
|
||||
value = (e.target as HTMLInputElement).checked;
|
||||
} else if (e.target.type === 'number') {
|
||||
value = e.target.value === '' ? '' : Number(e.target.value);
|
||||
}
|
||||
|
||||
// Apply transformation if provided
|
||||
if (transform) {
|
||||
value = transform(value);
|
||||
}
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
value,
|
||||
dirty: true,
|
||||
touched: true,
|
||||
}));
|
||||
},
|
||||
[transform]
|
||||
);
|
||||
|
||||
const setValue = useCallback((value: T) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
value,
|
||||
dirty: true,
|
||||
touched: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setError = useCallback((error: string | null) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error,
|
||||
isValid: !error,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setTouched = useCallback((touched: boolean) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
touched,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: null,
|
||||
isValid: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
value: initialValue,
|
||||
error: null,
|
||||
touched: false,
|
||||
dirty: false,
|
||||
isValid: true,
|
||||
});
|
||||
}, [initialValue]);
|
||||
|
||||
// Auto-validate on mount if initial value exists
|
||||
// This ensures initial values are validated
|
||||
// Note: We're intentionally not adding initialValue to dependencies
|
||||
// to avoid infinite loops, but we validate once on mount
|
||||
// This is handled by the updateState function when value changes
|
||||
|
||||
return {
|
||||
value: state.value,
|
||||
error: state.error,
|
||||
touched: state.touched,
|
||||
dirty: state.dirty,
|
||||
isValid: state.isValid,
|
||||
handleChange,
|
||||
setValue,
|
||||
setError,
|
||||
setTouched,
|
||||
reset,
|
||||
clearError,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing form field state with additional utilities
|
||||
*/
|
||||
export function useFormFieldWithHelpers<T = string>(
|
||||
options: FormFieldOptions<T> & {
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
helpText?: string;
|
||||
} = {}
|
||||
) {
|
||||
const field = useFormField<T>(options);
|
||||
|
||||
const hasError = field.error !== null;
|
||||
const showError = field.touched && hasError;
|
||||
const showSuccess = field.touched && !hasError && field.dirty;
|
||||
|
||||
const getAriaDescribedBy = () => {
|
||||
const descriptions: string[] = [];
|
||||
if (options.helpText) descriptions.push(`${options.label || 'field'}-help`);
|
||||
if (field.error) descriptions.push(`${options.label || 'field'}-error`);
|
||||
return descriptions.length > 0 ? descriptions.join(' ') : undefined;
|
||||
};
|
||||
|
||||
const getInputProps = () => ({
|
||||
value: field.value as any,
|
||||
onChange: field.handleChange,
|
||||
'aria-invalid': hasError,
|
||||
'aria-describedby': getAriaDescribedBy(),
|
||||
'aria-required': options.required,
|
||||
});
|
||||
|
||||
const getLabelProps = () => ({
|
||||
htmlFor: options.label?.toLowerCase().replace(/\s+/g, '-'),
|
||||
required: options.required,
|
||||
});
|
||||
|
||||
return {
|
||||
...field,
|
||||
hasError,
|
||||
showError,
|
||||
showSuccess,
|
||||
getInputProps,
|
||||
getLabelProps,
|
||||
getAriaDescribedBy,
|
||||
};
|
||||
}
|
||||
264
components/forms/hooks/useFormValidation.ts
Normal file
264
components/forms/hooks/useFormValidation.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user