UNPKG

@matthew.ngo/reform

Version:

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

279 lines (245 loc) 8.82 kB
import { useEffect, useRef, useCallback } from 'react'; import { UseFormWatch } from 'react-hook-form'; import { FieldWatchConfig, FieldWatcherReturn } from './types'; import { useMemoizedCallback } from '../../common/useMemoizedCallback'; import { FormGroup } from '../../core/form/form-groups'; /** * Hook for watching specific field changes across form groups * * @template T - Form data type * @param watch - React Hook Form watch function * @returns Object with field watching utilities * * @example * const form = useReform(config); * const { watch } = form.formMethods; * const fieldWatcher = useFieldWatcher<FormData>(watch); * * // Watch email field changes * useEffect(() => { * const unsubscribe = fieldWatcher.watchField({ * field: 'email', * callback: (value) => validateEmailAsync(value), * debounce: 500 * }); * * return unsubscribe; * }, []); */ export const useFieldWatcher = <T extends Record<string, any>>( watch: UseFormWatch<{ groups: FormGroup<T>[] }> ): FieldWatcherReturn<T> => { // Store active watchers const watchersRef = useRef< Map< string, { config: FieldWatchConfig<T, any>; previousValues: Record<number, any>; timeoutId?: NodeJS.Timeout; } > >(new Map()); // Store latest form data const formDataRef = useRef<{ groups: FormGroup<T>[] } | null>(null); // Create a unique key for each watcher const createWatcherKey = (field: keyof T, groupIndex?: number) => { return `${String(field)}${ groupIndex !== undefined ? `:${groupIndex}` : '' }`; }; // Process field changes const processFieldChange = useCallback( <K extends keyof T>( field: K, groupIndex: number, value: T[K], formData: { groups: FormGroup<T>[] } ) => { // Check for watchers that match this field and group const specificKey = createWatcherKey(field, groupIndex); const globalKey = createWatcherKey(field); // Update form data reference formDataRef.current = formData; // Process specific group watcher if (watchersRef.current.has(specificKey)) { const watcher = watchersRef.current.get(specificKey)!; const previousValue = watcher.previousValues[groupIndex]; // Only trigger if value actually changed if (value !== previousValue) { const { callback, debounce } = watcher.config; // Clear any existing timeout if (watcher.timeoutId) { clearTimeout(watcher.timeoutId); } // Execute callback with debounce if specified if (debounce && debounce > 0) { watcher.timeoutId = setTimeout(() => { callback(value, previousValue, groupIndex, formData); // Update previous value after callback watcher.previousValues[groupIndex] = value; }, debounce); } else { // Execute immediately callback(value, previousValue, groupIndex, formData); // Update previous value after callback watcher.previousValues[groupIndex] = value; } } } // Process global field watcher (for all groups) if (watchersRef.current.has(globalKey)) { const watcher = watchersRef.current.get(globalKey)!; const previousValue = watcher.previousValues[groupIndex]; // Only trigger if value actually changed if (value !== previousValue) { const { callback, debounce } = watcher.config; // Clear any existing timeout if (watcher.timeoutId) { clearTimeout(watcher.timeoutId); } // Execute callback with debounce if specified if (debounce && debounce > 0) { watcher.timeoutId = setTimeout(() => { callback(value, previousValue, groupIndex, formData); // Update previous value after callback watcher.previousValues[groupIndex] = value; }, debounce); } else { // Execute immediately callback(value, previousValue, groupIndex, formData); // Update previous value after callback watcher.previousValues[groupIndex] = value; } } } }, [] ); // Set up the watch subscription useEffect(() => { // Subscribe to form changes const subscription = watch((formData, { name, type }) => { // Only process when we have groups data if (!formData?.groups) return; // Check if the change is related to a field if (type === 'change' && name && name.includes('.data.')) { // Extract group index and field name from the field path const match = name.match(/groups\.(\d+)\.data\.(.+)/); if (match && match[1] && match[2]) { const groupIndex = parseInt(match[1], 10); const fieldPath = match[2]; // Find the field that matches this path // This is a simplification - for nested fields you'd need more complex logic const field = fieldPath.split('.')[0] as keyof T; const group = formData.groups[groupIndex]; if (!group || !group.id) return; // Get the current value const currentValue = group.data?.[field as string]; if (currentValue !== undefined) { // Process the field change processFieldChange( field, groupIndex, currentValue, formData as { groups: FormGroup<T>[] } ); } } } }); // Clean up subscription return () => subscription.unsubscribe(); }, [watch, processFieldChange]); // Register a new field watcher const watchField = useMemoizedCallback( <K extends keyof T>(config: FieldWatchConfig<T, K>) => { const { field, groupIndex, immediate = false } = config; const key = createWatcherKey(field, groupIndex); // Store the watcher configuration watchersRef.current.set(key, { config, previousValues: {}, }); // Trigger immediately if requested and we have form data if (immediate && formDataRef.current?.groups) { const formData = formDataRef.current; if (groupIndex !== undefined) { // Trigger for specific group const value = formData.groups[groupIndex]?.data?.[field as string]; if (value !== undefined) { processFieldChange(field, groupIndex, value, formData); } } else { // Trigger for all groups formData.groups.forEach((group, idx) => { const value = group.data?.[field as string]; if (value !== undefined) { processFieldChange(field, idx, value, formData); } }); } } // Return unsubscribe function return () => { const watcher = watchersRef.current.get(key); if (watcher?.timeoutId) { clearTimeout(watcher.timeoutId); } watchersRef.current.delete(key); }; }, [processFieldChange] ); // Register multiple field watchers const watchFields = useMemoizedCallback( <K extends keyof T>(configs: FieldWatchConfig<T, K>[]) => { // Register each watcher const unsubscribers = configs.map(config => watchField(config)); // Return combined unsubscribe function return () => { unsubscribers.forEach(unsubscribe => unsubscribe()); }; }, [watchField] ); // Get current field value const getFieldValue = useMemoizedCallback( <K extends keyof T>(field: K, groupIndex: number) => { if (formDataRef.current?.groups?.[groupIndex]?.data) { return formDataRef.current.groups[groupIndex].data[field as string]; } return undefined; }, [] ); // Manually trigger field watchers const triggerFieldWatch = useMemoizedCallback( <K extends keyof T>(field: K, groupIndex?: number) => { if (!formDataRef.current?.groups) return; const formData = formDataRef.current; if (groupIndex !== undefined) { // Trigger for specific group const value = formData.groups[groupIndex]?.data?.[field as string]; if (value !== undefined) { processFieldChange(field, groupIndex, value, formData); } } else { // Trigger for all groups formData.groups.forEach((group, idx) => { const value = group.data?.[field as string]; if (value !== undefined) { processFieldChange(field, idx, value, formData); } }); } }, [processFieldChange] ); return { watchField, watchFields, getFieldValue, triggerFieldWatch, }; };