UNPKG

@matthew.ngo/reform

Version:

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

213 lines (193 loc) 6.8 kB
import { useMemo } from 'react'; import { ReformReturn } from '../../types'; import { useFieldWatcher } from './useFieldWatcher'; import { useFormStateObserver } from './useFormStateObserver'; import { FormStateObserverOptions } from './types'; /** * Hook wrapper for field watching and form state observation in Reform * * @template T - The type of form data * @param reform - Reform hook return value * @param options - Configuration options for form state observer * @returns Combined field watcher and form state observer utilities * * @example * // Basic usage * const form = useReform<UserForm>({...}); * const watcher = useReformWatcher(form); * * // Watch a specific field * useEffect(() => { * const unsubscribe = watcher.watchField({ * field: 'email', * callback: (value) => validateEmailAsync(value), * debounce: 500 * }); * * return unsubscribe; * }, [watcher]); * * // Subscribe to form state changes * useEffect(() => { * const subscription = watcher.subscribeToState((state) => { * if (state.isDirty) { * console.log('Form has unsaved changes'); * } * }); * * return subscription.unsubscribe; * }, [watcher]); */ export const useReformWatcher = <T extends Record<string, any>>( reform: ReformReturn<T>, options: FormStateObserverOptions<T> = {} ) => { // Extract methods from reform const { formMethods } = reform; const { watch, formState } = formMethods; // Initialize field watcher const fieldWatcher = useFieldWatcher<T>(watch); // Initialize form state observer const stateObserver = useFormStateObserver<T>(formState, options); // Create a combined API return useMemo(() => ({ // Field watching capabilities watchField: fieldWatcher.watchField, watchFields: fieldWatcher.watchFields, getFieldValue: fieldWatcher.getFieldValue, triggerFieldWatch: fieldWatcher.triggerFieldWatch, // Form state observation capabilities subscribeToState: stateObserver.subscribe, getFormState: stateObserver.getState, getPreviousState: stateObserver.getPreviousState, isDirty: stateObserver.isDirty, hasErrors: stateObserver.hasErrors, isSubmitting: stateObserver.isSubmitting, isValid: stateObserver.isValid, // Enhanced methods that combine both capabilities /** * Watch a field and update when form state changes * * @param config - Field watch configuration * @param stateHandler - Optional handler for form state changes * @returns Function to unsubscribe both watchers */ watchFieldWithState: ( config: Parameters<typeof fieldWatcher.watchField>[0], stateHandler?: Parameters<typeof stateObserver.subscribe>[0] ) => { const fieldUnsubscribe = fieldWatcher.watchField(config as any); let stateUnsubscribe = () => {}; if (stateHandler) { const subscription = stateObserver.subscribe(stateHandler); stateUnsubscribe = subscription.unsubscribe; } return () => { fieldUnsubscribe(); stateUnsubscribe(); }; }, /** * Watch a field only when the form is valid * * @param config - Field watch configuration * @returns Function to unsubscribe */ watchFieldWhenValid: ( config: Parameters<typeof fieldWatcher.watchField>[0] ) => { // Create a wrapper callback that only executes when form is valid const originalCallback = config.callback; const wrappedCallback: typeof originalCallback = (value, prevValue, groupIndex, formData) => { if (stateObserver.isValid()) { originalCallback(value, prevValue, groupIndex, formData); } }; // Return the field watcher with wrapped callback return fieldWatcher.watchField({ ...config, callback: wrappedCallback } as any); }, /** * Watch for changes in a specific field's error state * * @param field - Field to watch for errors * @param callback - Callback function when error state changes * @returns Function to unsubscribe */ watchFieldErrors: ( field: keyof T, callback: (hasError: boolean, errorMessage: string | null) => void ) => { return stateObserver.subscribe((state) => { const errors = state.errors; const fieldErrors: Record<string, any> = {}; // Look for errors in all groups // Use Object.entries to get type-safe access to the errors object Object.entries(errors || {}).forEach(([key, errorValue]) => { // Match pattern like groups.0.data.fieldName const match = key.match(/groups\.(\d+)\.data\.(.+)/); if (match && match[2].split('.')[0] === String(field)) { const groupIndex = parseInt(match[1], 10); if (!fieldErrors[groupIndex]) { fieldErrors[groupIndex] = errorValue; } } }); // Call callback with error information const hasAnyError = Object.keys(fieldErrors).length > 0; const firstErrorMessage = hasAnyError ? Object.values(fieldErrors)[0]?.message || 'Invalid field' : null; callback(hasAnyError, firstErrorMessage); }); }, /** * Create a debounced auto-save function that triggers when form becomes dirty * * @param saveFunction - Function to call when auto-save triggers * @param debounceMs - Debounce delay in milliseconds * @returns Object with control methods */ createAutoSave: ( saveFunction: () => void | Promise<void>, debounceMs: number = 1000 ) => { let timeoutId: NodeJS.Timeout | null = null; let isEnabled = true; // Subscribe to form state changes const subscription = stateObserver.subscribe((state, prevState) => { // Only trigger when form becomes or remains dirty if (!isEnabled || !state.isDirty) return; // Clear any existing timeout if (timeoutId) { clearTimeout(timeoutId); } // Set new timeout timeoutId = setTimeout(() => { saveFunction(); }, debounceMs); }); // Return control methods return { stop: () => { isEnabled = false; if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }, start: () => { isEnabled = true; }, trigger: () => { if (isEnabled) { saveFunction(); } }, unsubscribe: subscription.unsubscribe }; } }), [fieldWatcher, stateObserver]); };