migration wip
This commit is contained in:
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user