UNPKG

@matthew.ngo/reform

Version:

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

298 lines (272 loc) 8.1 kB
import { FieldValues, useFieldArray, UseFormReturn } from 'react-hook-form'; import { FormDataPath } from '../../types'; import { FormGroup } from './form-groups'; import { useCallback, useMemo } from 'react'; import { useMemoizedCallback } from '../../common/useMemoizedCallback'; import { ValidationResult } from '../validation'; import { ReformGroupHandler } from '../../typessss'; interface UseReformGroupsProps<T extends Record<string, any>> { methods: UseFormReturn<{ groups: FormGroup<T>[] }>; minGroups: number; maxGroups: number; defaultData: T; onChange?: (groups: FormGroup<T>[]) => void; } // Memoize this function to prevent recreation on each render const createPathString = (index: number, field: string): string => { return `groups.${index}.data.${field}`; }; export const useReformGroups = <T extends Record<string, any> & FieldValues>({ methods, minGroups, maxGroups, defaultData, onChange, }: UseReformGroupsProps<T>): ReformGroupHandler<T> => { // Memoize defaultData to prevent unnecessary re-renders const memoizedDefaultData = useMemo(() => defaultData, [ // Stringify defaultData to compare by value instead of reference JSON.stringify(defaultData), ]); const { fields, append, remove, move, update, replace } = useFieldArray({ control: methods.control, name: 'groups', }); // Memoize this function to prevent recreation on each render const createNewGroup = useCallback( (data: T = memoizedDefaultData): FormGroup<T> => ({ id: Math.random() .toString(36) .substring(2, 9), data: { ...data }, }), [memoizedDefaultData] ); const addGroup = useMemoizedCallback(async (): Promise<ValidationResult> => { if (fields.length >= maxGroups) { return { isValid: false, message: `Maximum ${maxGroups} groups allowed`, }; } append(createNewGroup()); const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [ fields.length, maxGroups, append, createNewGroup, methods.trigger, methods.getValues, onChange, ]); const removeGroup = useMemoizedCallback( async (index: number): Promise<ValidationResult> => { if (fields.length <= minGroups) { return { isValid: false, message: `Minimum ${minGroups} groups required`, }; } remove(index); const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [ fields.length, minGroups, remove, methods.trigger, methods.getValues, onChange, ] ); const moveGroup = useMemoizedCallback( async (from: number, to: number): Promise<ValidationResult> => { if (from < 0 || from >= fields.length || to < 0 || to >= fields.length) { return { isValid: false, message: 'Invalid group indices' }; } move(from, to); const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [fields.length, move, methods.trigger, methods.getValues, onChange] ); const duplicateGroup = useMemoizedCallback( async (index: number): Promise<ValidationResult> => { if (fields.length >= maxGroups) { return { isValid: false, message: `Maximum ${maxGroups} groups allowed`, }; } const groupToDuplicate = methods.getValues(`groups.${index}`); if (!groupToDuplicate) { return { isValid: false, message: 'Group not found' }; } append(createNewGroup(groupToDuplicate.data)); const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [ fields.length, maxGroups, methods.getValues, append, createNewGroup, methods.trigger, onChange, ] ); // Batch operations const batchUpdate = useMemoizedCallback( async ( indices: number[], updater: (data: T) => Partial<T> ): Promise<ValidationResult> => { // Validate indices const invalidIndices = indices.filter( index => index < 0 || index >= fields.length ); if (invalidIndices.length > 0) { return { isValid: false, message: `Invalid indices: ${invalidIndices.join(', ')}`, }; } // Update each group for (const index of indices) { const group = methods.getValues(`groups.${index}`); if (group) { const updatedData = { ...group.data, ...updater(group.data), }; update(index, { ...group, data: updatedData, }); } } // Validate and notify const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [fields.length, methods.getValues, methods.trigger, update, onChange] ); const batchRemove = useMemoizedCallback( async (indices: number[]): Promise<ValidationResult> => { // Sort indices in descending order to avoid index shifting issues const sortedIndices = [...indices].sort((a, b) => b - a); // Check if removing would violate minimum groups constraint if (fields.length - sortedIndices.length < minGroups) { return { isValid: false, message: `Cannot remove ${sortedIndices.length} groups. Minimum ${minGroups} groups required.`, }; } // Validate indices const invalidIndices = sortedIndices.filter( index => index < 0 || index >= fields.length ); if (invalidIndices.length > 0) { return { isValid: false, message: `Invalid indices: ${invalidIndices.join(', ')}`, }; } // Remove groups one by one for (const index of sortedIndices) { remove(index); } // Validate and notify const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [ fields.length, minGroups, remove, methods.trigger, methods.getValues, onChange, ] ); /** * Get all form groups */ const getGroups = useCallback((): FormGroup<T>[] => { return methods.getValues().groups || []; }, [methods]); /** * Set all form groups * * @param groups - New groups to set * @returns Promise with validation result */ const setGroups = useCallback( async (groups: FormGroup<T>[]): Promise<ValidationResult> => { // Validate groups count if (groups.length < minGroups) { return { isValid: false, message: `Minimum ${minGroups} groups required`, }; } if (groups.length > maxGroups) { return { isValid: false, message: `Maximum ${maxGroups} groups allowed`, }; } // Replace all groups replace(groups); // Validate and notify const isValid = await methods.trigger(); onChange?.(methods.getValues().groups); return { isValid }; }, [methods, minGroups, maxGroups, replace, onChange] ); return useMemo( () => ({ groups: fields as unknown as FormGroup<T>[], // Type assertion to fix compatibility issue canAddGroup: fields.length < maxGroups, canRemoveGroup: fields.length > minGroups, register: <K extends string>(index: number, field: K, options?: any) => methods.register( createPathString(index, field) as FormDataPath<T>, options ), getGroups, setGroups, addGroup, removeGroup, moveGroup, duplicateGroup, batchUpdate, batchRemove, }), [ fields, maxGroups, minGroups, methods.register, getGroups, setGroups, addGroup, removeGroup, moveGroup, duplicateGroup, batchUpdate, batchRemove, ] ); };