UNPKG

@matthew.ngo/reform

Version:

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

187 lines (160 loc) 5.55 kB
import { useMemo, useRef, useState } from 'react'; import { ReformReturn } from '../../../types'; import { DynamicSchemaConfig, DynamicValidationRule, ValidationContext, ValidationRuleResult, DynamicSchemaReturn, } from './types'; import { FieldPath } from '../../form/form-groups'; import get from 'lodash/get'; import { useMemoizedCallback } from '../../../common/useMemoizedCallback'; /** * Hook for managing dynamic schema validation * * @template T - The type of form data * @param reform - The Reform hook return value * @param config - Configuration for dynamic schema validation * @returns Dynamic schema validation state and methods */ export const useDynamicValidation = <T extends Record<string, any>>( reform: ReformReturn<T>, config: DynamicSchemaConfig<T> ): DynamicSchemaReturn<T> => { // Store reform reference to avoid unnecessary re-renders const reformRef = useRef(reform); // Memoize config to prevent unnecessary re-renders const memoizedConfig = useMemo( () => ({ validations: config.validations, getContextData: config.getContextData, }), [ // Use JSON.stringify for complex objects to compare by value JSON.stringify(config.validations), config.getContextData, ] ); const { validations, getContextData } = memoizedConfig; // State for additional context data const [contextData, setContextData] = useState<Record<string, any>>( getContextData?.() || {} ); // Create validation context for a specific group - memoized to prevent unnecessary re-renders const createContext = useMemoizedCallback( (groupIndex: number): ValidationContext<T> => { const groups = reformRef.current.getGroups(); const currentGroup = groups[groupIndex] || { id: '', data: {} as T }; return { groups, currentGroup, groupIndex, contextData, }; }, [contextData] ); // Get applicable validation rules for a field - memoized to prevent unnecessary re-renders const getFieldRules = useMemoizedCallback( (groupIndex: number, field: FieldPath<T>): DynamicValidationRule<T>[] => { const fieldValidation = validations.find(v => v.field === field); if (!fieldValidation) return []; const context = createContext(groupIndex); const value = get(context.currentGroup.data, field); // Filter rules based on conditions return fieldValidation.rules.filter(rule => { if (!rule.when) return true; return rule.when(value, context); }); }, [validations, createContext] ); // Validate a specific field - memoized to prevent unnecessary re-renders const validateField = useMemoizedCallback( ( groupIndex: number, field: FieldPath<T>, value: any ): ValidationRuleResult => { const context = createContext(groupIndex); const rules = getFieldRules(groupIndex, field); // No rules to apply if (rules.length === 0) { return { isValid: true }; } // Apply each rule until one fails for (const rule of rules) { const result = rule.validate(value, context); if (result === false || typeof result === 'string') { let message: string; if (typeof result === 'string') { message = result; } else if (typeof rule.message === 'function') { message = rule.message(value, context); } else { message = rule.message || 'Validation failed'; } return { isValid: false, message }; } } return { isValid: true }; }, [createContext, getFieldRules] ); // Validate an entire group - memoized to prevent unnecessary re-renders const validateGroup = useMemoizedCallback( (groupIndex: number): boolean => { const context = createContext(groupIndex); const { currentGroup } = context; // Get all fields that have validation rules const fieldsToValidate = validations .map(v => v.field) .filter((field, index, self) => self.indexOf(field) === index); // Validate each field for (const field of fieldsToValidate) { const value = get(currentGroup.data, field); const result = validateField(groupIndex, field, value); if (!result.isValid) { return false; } } return true; }, [createContext, validations, validateField] ); // Update context data - memoized to prevent unnecessary re-renders const updateContext = useMemoizedCallback( (newContextData: Record<string, any>) => { setContextData(prev => ({ ...prev, ...newContextData, })); }, [] ); // Register validation with Reform useMemo(() => { // Update reform reference reformRef.current = reform; // Register custom validation for each field validations.forEach(fieldValidation => { const { field } = fieldValidation; // FIXME // reform.registerFieldValidator(field, (value, groupIndex) => { // const result = validateField(groupIndex, field, value); // return result.isValid ? true : result.message || 'Invalid value'; // }); }); }, [reform, validations, validateField]); // Return memoized object to prevent unnecessary re-renders return useMemo( () => ({ validateField, validateGroup, getFieldRules, updateContext, }), [validateField, validateGroup, getFieldRules, updateContext] ); };