UNPKG

@matthew.ngo/reform

Version:

A flexible and powerful React form management library with advanced validation, state observation, and multi-group support

307 lines (268 loc) 7.89 kB
import { UseFormReturn, Path } from "react-hook-form"; import { useCallback, useRef, useEffect, useMemo } from "react"; /** * Import the FormGroup type from base types */ import { useMemoizedCallback } from "../../common/useMemoizedCallback"; import { FieldArrayHelpers } from "./types"; import { FormGroup } from '../../core/form/form-groups'; /** * Props for the useFieldArrayHelpers hook * * @template T - The type of form data within each group */ interface UseFieldArrayHelpersProps<T> { /** React Hook Form methods for the form */ methods: UseFormReturn<{ groups: FormGroup<T>[] }>; /** Optional callback triggered when array fields are modified */ onChange?: (groups: FormGroup<T>[]) => void; } /** * Hook that provides utilities for managing array fields within form groups * * This hook enables manipulation of array fields inside Reform groups, providing * methods similar to React Hook Form's useFieldArray but adapted for the Reform * group structure. * * @template T - The type of form data within each group */ export const useFieldArrayHelpers = <T extends Record<string, any>>({ methods, onChange, }: UseFieldArrayHelpersProps<T>): FieldArrayHelpers<T> => { // Store methods reference to avoid unnecessary re-renders const methodsRef = useRef(methods); useEffect(() => { methodsRef.current = methods; }, [methods]); // Store onChange reference to avoid unnecessary re-renders const onChangeRef = useRef(onChange); useEffect(() => { onChangeRef.current = onChange; }, [onChange]); /** * Creates a properly typed path for accessing array fields * * @param index - The group index * @param field - The field name containing the array * @returns A type-safe path string for React Hook Form */ const createArrayPath = useMemoizedCallback( <K extends keyof T>( index: number, field: K ): Path<{ groups: FormGroup<T>[] }> => { return `groups.${index}.data.${String(field)}` as Path<{ groups: FormGroup<T>[]; }>; }, [] ); /** * Gets the array value for a specific field */ const getArray = useMemoizedCallback( <K extends keyof T>(index: number, field: K): any[] => { const path = createArrayPath(index, field); const value = methodsRef.current.getValues(path); return Array.isArray(value) ? value : []; }, [createArrayPath] ); /** * Notifies about changes if onChange callback is provided */ const notifyChange = useMemoizedCallback(() => { if (onChangeRef.current) { onChangeRef.current(methodsRef.current.getValues().groups); } }, []); /** * Appends a new value to the end of an array field */ const append = useMemoizedCallback( <K extends keyof T>( index: number, field: K, value: any, options?: { shouldFocus?: boolean } ) => { const path = createArrayPath(index, field); const currentArray = getArray(index, field); methodsRef.current.setValue(path, [...currentArray, value], { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, getArray, notifyChange] ); /** * Removes an item from an array field at the specified index */ const remove = useMemoizedCallback( <K extends keyof T>(index: number, field: K, arrayIndex: number) => { const path = createArrayPath(index, field); const currentArray = getArray(index, field); if (arrayIndex < 0 || arrayIndex >= currentArray.length) { return; } const newArray = [ ...currentArray.slice(0, arrayIndex), ...currentArray.slice(arrayIndex + 1), ]; methodsRef.current.setValue(path, newArray, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, getArray, notifyChange] ); /** * Updates an item in an array field at the specified index */ const update = useMemoizedCallback( <K extends keyof T>( index: number, field: K, arrayIndex: number, value: any ) => { const path = createArrayPath(index, field); const currentArray = getArray(index, field); if (arrayIndex < 0 || arrayIndex >= currentArray.length) { return; } const newArray = [...currentArray]; newArray[arrayIndex] = value; methodsRef.current.setValue(path, newArray, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, getArray, notifyChange] ); /** * Moves an item from one position to another within an array field */ const move = useMemoizedCallback( <K extends keyof T>(index: number, field: K, from: number, to: number) => { const path = createArrayPath(index, field); const currentArray = getArray(index, field); if ( from < 0 || from >= currentArray.length || to < 0 || to >= currentArray.length ) { return; } const newArray = [...currentArray]; const [movedItem] = newArray.splice(from, 1); newArray.splice(to, 0, movedItem); methodsRef.current.setValue(path, newArray, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, getArray, notifyChange] ); /** * Swaps two items in an array field */ const swap = useMemoizedCallback( <K extends keyof T>( index: number, field: K, indexA: number, indexB: number ) => { const path = createArrayPath(index, field); const currentArray = getArray(index, field); if ( indexA < 0 || indexA >= currentArray.length || indexB < 0 || indexB >= currentArray.length ) { return; } const newArray = [...currentArray]; const temp = newArray[indexA]; newArray[indexA] = newArray[indexB]; newArray[indexB] = temp; methodsRef.current.setValue(path, newArray, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, getArray, notifyChange] ); /** * Inserts a new value at a specific position in an array field */ const insert = useMemoizedCallback( <K extends keyof T>( index: number, field: K, arrayIndex: number, value: any ) => { const path = createArrayPath(index, field); const currentArray = getArray(index, field); if (arrayIndex < 0 || arrayIndex > currentArray.length) { return; } const newArray = [ ...currentArray.slice(0, arrayIndex), value, ...currentArray.slice(arrayIndex), ]; methodsRef.current.setValue(path, newArray, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, getArray, notifyChange] ); /** * Replaces the entire array with a new array */ const replace = useMemoizedCallback( <K extends keyof T>(index: number, field: K, newArray: any[]) => { const path = createArrayPath(index, field); methodsRef.current.setValue(path, newArray, { shouldValidate: true, shouldDirty: true, shouldTouch: true, }); notifyChange(); }, [createArrayPath, notifyChange] ); // Memoize the return object to prevent unnecessary re-renders return useMemo( () => ({ getArray, append, remove, update, move, swap, insert, replace, }), [getArray, append, remove, update, move, swap, insert, replace] ); };