import { useState, useCallback, ChangeEvent } from 'react'; /** * Hook for managing individual form field state */ export interface FormFieldState { value: T; error: string | null; touched: boolean; dirty: boolean; isValid: boolean; } export interface FormFieldOptions { initialValue?: T; validate?: (value: T) => string | null; transform?: (value: T) => T; } export interface FormFieldReturn { value: T; error: string | null; touched: boolean; dirty: boolean; isValid: boolean; handleChange: (e: ChangeEvent) => 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( options: FormFieldOptions = {} ): FormFieldReturn { const { initialValue = '' as unknown as T, validate, transform, } = options; const [state, setState] = useState>({ 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>) => { 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) => { 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( options: FormFieldOptions & { label?: string; required?: boolean; helpText?: string; } = {} ) { const field = useFormField(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, }; }