UNPKG

siddhesh-10-react-form-binder

Version:

A React hook that simplifies form state management by providing Angular-like two-way binding, nested state support, and validation—all with TypeScript support.

169 lines (155 loc) • 5.54 kB
import { useState, useCallback, ChangeEvent } from 'react'; /** * A type for validator functions. * Given the current field value and all form values, * it returns an error message string if invalid, or undefined if valid. */ export type Validator<T> = (value: any, values: T) => string | undefined; /** * A mapping of field paths (can be nested using dot-notation) to validator functions. */ export type Validators<T> = { [fieldPath: string]: Validator<T>; }; /** * A type for the binding return object. */ export interface FieldBinding { value: any; onChange: (e: ChangeEvent<any>) => void; // Optionally you can add onBlur or other event handlers here. } /** * A type for the hook return value. */ export interface UseFormBinderReturn<T> { values: T; errors: Record<string, string | undefined>; bind: (fieldPath: string) => FieldBinding; /** * Validates the entire form. Returns true if all fields are valid, * otherwise false. It also updates the errors state. */ validate: () => boolean; } /** * Utility: Retrieve a nested property value using a path array. */ function getIn(obj: any, path: string[]): any { return path.reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), obj); } /** * Utility: Produce a new object with a nested property updated. * * @param obj - The original object. * @param path - An array of keys representing the nested path. * @param value - The new value to set at the given path. * @returns A new object with the nested property updated. */ function setIn<T>(obj: T, path: string[], value: any): T { if (path.length === 0) return obj; const [head, ...tail] = path; // Ensure the current level is an object; if not, create one. const currentLevel = (obj as any)[head] ?? (tail.length ? {} : undefined); return { ...obj, [head]: tail.length > 0 ? setIn(currentLevel, tail, value) : value, }; } /** * useFormBinder * * A custom hook for managing form state with support for: * - Automatic binding of input values (even nested, using dot-notation) * - Field-level validation (validators run on each change) * - A full-form validation function * * @param initialValues - The initial values for your form. * @param validators - An optional object mapping field paths to validator functions. * * @returns An object containing: * - values: The current form state. * - errors: The current error messages for each field (if any). * - bind: A function that returns the props for an input field. * - validate: A function that validates the entire form. * * @example * // Using the hook in a React component: * const initialValues = { * name: '', * email: '', * user: { age: 0 } * }; * * const validators = { * 'name': (value) => value.trim() === '' ? 'Name is required' : undefined, * 'email': (value) => /\S+@\S+\.\S+/.test(value) ? undefined : 'Invalid email', * 'user.age': (value) => value < 18 ? 'Must be 18 or older' : undefined, * }; * * const { values, errors, bind, validate } = useFormBinder(initialValues, validators); */ export function useFormBinder<T extends object>( initialValues: T, validators?: Validators<T> ): UseFormBinderReturn<T> { const [values, setValues] = useState<T>(initialValues); const [errors, setErrors] = useState<Record<string, string | undefined>>({}); // Helper to run a validator for a given field (if it exists) const runValidator = (fieldPath: string, fieldValue: any, allValues: T): string | undefined => { if (validators && validators[fieldPath]) { return validators[fieldPath](fieldValue, allValues); } return undefined; }; // bind: returns props to bind to an input field. const bind = useCallback( (fieldPath: string): FieldBinding => { const path = fieldPath.split('.'); // Retrieve current field value (using nested access) const fieldValue = getIn(values, path); return { value: fieldValue, onChange: (e: ChangeEvent<any>) => { const newValue = e.target.type === 'checkbox' ? e.target.checked : e.target.value; // Update the values state immutably using setIn helper setValues((prevValues) => { const updatedValues = setIn(prevValues, path, newValue); // Run validator for this field and update errors accordingly const errorMsg = runValidator(fieldPath, newValue, updatedValues); setErrors((prevErrors) => ({ ...prevErrors, [fieldPath]: errorMsg })); return updatedValues; }); }, }; }, [values, validators] ); /** * validate: Runs all validators for the form. * Updates the errors state. * @returns true if no errors are found; false otherwise. */ const validate = useCallback((): boolean => { const newErrors: Record<string, string | undefined> = {}; let isValid = true; if (validators) { for (const fieldPath in validators) { if (Object.prototype.hasOwnProperty.call(validators, fieldPath)) { const path = fieldPath.split('.'); const fieldValue = getIn(values, path); const errorMsg = validators[fieldPath](fieldValue, values); newErrors[fieldPath] = errorMsg; if (errorMsg) { isValid = false; } } } } setErrors(newErrors); return isValid; }, [values, validators]); return { values, errors, bind, validate }; } export default useFormBinder;