el-form-react-hooks
Version:
React Hook Form alternative - TypeScript-first useForm hook with enterprise-grade state management. Schema-agnostic validation (Zod, Yup, Valibot), minimal re-renders, advanced form controls.
1 lines • 91 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/useForm.ts","../src/utils/index.ts","../src/utils/arrayHelpers.ts","../src/utils/equality.ts","../src/utils/dirtyState.ts","../src/utils/validation.ts","../src/utils/fieldOperations.ts","../src/utils/formState.ts","../src/utils/submitOperations.ts","../src/utils/errorManagement.ts","../src/utils/formHistory.ts","../src/utils/focusManagement.ts","../src/utils/arrayOperations.ts","../src/utils/fileUtils.ts","../src/FormContext.tsx","../src/SubscriptionContext.tsx","../src/useFormSelector.ts","../src/useField.ts"],"sourcesContent":["// Re-export everything from core for convenience\nexport * from \"el-form-core\";\n\n// Export React-specific hooks\nexport { useForm } from \"./useForm\";\nexport {\n FormProvider,\n useFormContext,\n useFormState,\n useDiscriminatedUnionContext,\n} from \"./FormContext\";\nexport { useFormSelector } from \"./useFormSelector\";\nexport { useField } from \"./useField\";\nexport { shallowEqual } from \"./utils\";\nexport type {\n UseFormOptions,\n UseFormReturn,\n FormState,\n FieldState,\n ResetOptions,\n SetFocusOptions,\n FormContextValue,\n} from \"./types\";\nexport type { Path, PathValue, RegisterReturn } from \"./types/path\";\n","import { useState, useCallback, useRef } from \"react\";\nimport type { Path, PathValue, RegisterReturn } from \"./types/path\";\nimport {\n FormState,\n UseFormOptions,\n UseFormReturn,\n ResetOptions,\n} from \"./types\";\nimport {\n setNestedValue,\n getNestedValue,\n ValidationEngine,\n createFileValidator,\n} from \"el-form-core\";\nimport {\n createDirtyStateManager,\n createValidationManager,\n createFieldOperationsManager,\n createFormStateManager,\n createSubmitOperationsManager,\n createErrorManagementManager,\n createFormHistoryManager,\n createFocusManager,\n createArrayOperationsManager,\n getFileInfo,\n getFilePreview,\n} from \"./utils\";\n\nexport function useForm<T extends Record<string, any>>(\n options: UseFormOptions<T>\n): UseFormReturn<T> {\n const {\n defaultValues = {},\n validators = {},\n fieldValidators = {},\n fileValidators = {},\n mode = \"onSubmit\",\n validateOn,\n onSubmit,\n schema,\n } = options;\n\n // Core refs and state\n const validationEngine = useRef(new ValidationEngine());\n const fieldRefs = useRef<\n Map<keyof T, HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>\n >(new Map());\n const dirtyFieldsRef = useRef<Set<string>>(new Set());\n const formStateRef = useRef<FormState<T>>();\n\n const [formState, setFormState] = useState<FormState<T>>({\n values: defaultValues,\n errors: {},\n touched: {},\n isSubmitting: false,\n isValid: false,\n isDirty: false,\n });\n\n // Separate state for file previews\n const [filePreview, setFilePreview] = useState<\n Partial<Record<keyof T, string | null>>\n >({});\n\n // Compute canSubmit directly as a derived value\n const canSubmit = formState.isValid && !formState.isSubmitting;\n\n // Keep ref current\n formStateRef.current = formState;\n\n // Create utility managers\n const dirtyManager = createDirtyStateManager<T>(dirtyFieldsRef);\n const validationManager = createValidationManager<T>({\n validationEngine,\n validators,\n fieldValidators,\n mode,\n validateOn,\n schema, // Pass schema for discriminated union validation\n });\n\n const fieldOperations = createFieldOperationsManager<T>({\n formState,\n setFormState,\n dirtyManager,\n defaultValues,\n });\n\n const formStateManager = createFormStateManager<T>({\n formState,\n setFormState,\n dirtyManager,\n defaultValues,\n });\n\n const submitOperations = createSubmitOperationsManager<T>({\n formState,\n setFormState,\n validationManager,\n onSubmit,\n });\n\n const errorManagement = createErrorManagementManager<T>({\n formState,\n setFormState,\n validationManager,\n });\n\n const formHistory = createFormHistoryManager<T>({\n formState,\n setFormState,\n dirtyManager,\n defaultValues,\n });\n\n const focusManager = createFocusManager<T>({\n fieldRefs,\n });\n\n const arrayOperations = createArrayOperationsManager<T>({\n setFormState,\n dirtyManager,\n });\n\n // Register field function - main registration logic (base implementation)\n const registerImpl = useCallback(\n (name: string) => {\n const fieldName = name as keyof T;\n const fieldValue =\n getNestedValue(formStateRef.current?.values || {}, name) ?? \"\";\n const isCheckbox = typeof fieldValue === \"boolean\";\n\n // Note: File input detection will be done via event.target.type in onChange\n\n const handleFileChange = async (\n name: string,\n value: File | FileList | File[] | null\n ) => {\n // File-specific validation using el-form-core\n const fileValidationOptions =\n fileValidators && fieldName in fileValidators\n ? (fileValidators as any)[fieldName]\n : undefined;\n if (fileValidationOptions && value) {\n const fileValidator = createFileValidator(fileValidationOptions);\n const validationError = fileValidator({\n value,\n fieldName: fieldName as string,\n values: formState.values as Record<string, any>,\n });\n\n if (validationError) {\n setFormState((prev) => ({\n ...prev,\n errors: { ...prev.errors, [fieldName]: validationError },\n isValid: false,\n }));\n return;\n }\n }\n\n // Generate preview for single file\n let preview: string | null = null;\n if (value instanceof File) {\n preview = await getFilePreview(value);\n }\n\n // Use extracted utility for dirty state\n dirtyManager.updateFieldDirtyState(name, value, defaultValues);\n\n // Calculate new values first\n const newValues = name.includes(\".\")\n ? setNestedValue(formState.values, name, value)\n : { ...formState.values, [name]: value };\n\n // Update file preview separately\n setFilePreview((prevPreviews) => {\n const newFilePreview = { ...prevPreviews };\n if (preview !== undefined) {\n newFilePreview[fieldName] = preview;\n } else if (!value) {\n // Clear preview if no file\n delete newFilePreview[fieldName];\n }\n return newFilePreview;\n });\n\n let newErrors = { ...formState.errors };\n\n // Clear field error\n if (name.includes(\".\")) {\n const nestedError = getNestedValue(newErrors, name);\n if (nestedError) {\n newErrors = setNestedValue(newErrors, name, undefined);\n }\n } else {\n delete newErrors[fieldName];\n }\n\n // Run Zod validation if configured\n if (validationManager.shouldValidate(\"onChange\")) {\n const validationResult = await validationManager.validateField(\n fieldName,\n value,\n newValues,\n \"onChange\"\n );\n\n if (!validationResult.isValid) {\n newErrors = { ...newErrors, ...validationResult.errors };\n }\n }\n\n setFormState((prev) => ({\n ...prev,\n values: newValues,\n errors: newErrors,\n isDirty: dirtyFieldsRef.current.size > 0,\n }));\n };\n\n const baseProps = {\n name,\n onChange: async (e: React.ChangeEvent<any>) => {\n const value = (() => {\n // Handle file inputs\n if (e.target.type === \"file\") {\n const files = e.target.files;\n const fileValue = e.target.multiple\n ? Array.from(files || []) // Convert FileList to array for multiple\n : files?.[0] || null; // Single File or null\n\n // Handle file change separately\n handleFileChange(name, fileValue);\n return; // Don't continue with regular value processing\n }\n\n if (isCheckbox) return e.target.checked;\n if (e.target.type === \"number\") {\n const num = e.target.valueAsNumber;\n // Handle empty number inputs - return undefined instead of empty string\n // This ensures that empty inputs for number fields are treated as `undefined`,\n // which is consistent with how optional fields are typically handled in forms\n // and prevents Zod validation errors for empty optional number fields.\n if (isNaN(num)) {\n return e.target.value === \"\" ? undefined : e.target.value;\n }\n return num;\n }\n return e.target.value;\n })();\n\n // Skip regular processing for file inputs as it's handled above\n if (e.target.type === \"file\") return;\n\n // Use extracted utility for dirty state\n dirtyManager.updateFieldDirtyState(name, value, defaultValues);\n\n setFormState((prev) => {\n const newValues = name.includes(\".\")\n ? setNestedValue(prev.values, name, value)\n : { ...prev.values, [name]: value };\n\n let newErrors = { ...prev.errors };\n\n // Clear field error\n if (name.includes(\".\")) {\n const nestedError = getNestedValue(newErrors, name);\n if (nestedError) {\n newErrors = setNestedValue(newErrors, name, undefined);\n }\n } else {\n delete newErrors[fieldName];\n }\n\n return {\n ...prev,\n values: newValues,\n errors: newErrors,\n isDirty: dirtyFieldsRef.current.size > 0,\n };\n });\n\n // Use extracted validation utility\n const shouldValidateResult =\n validationManager.shouldValidate(\"onChange\");\n\n if (shouldValidateResult) {\n const updatedValues = name.includes(\".\")\n ? setNestedValue(formState.values, name, value)\n : { ...formState.values, [name]: value };\n\n const result = await validationManager.validateField(\n fieldName,\n value,\n updatedValues,\n \"onChange\"\n );\n\n // Always update form state with validation results\n setFormState((prev) => {\n const newErrors = { ...prev.errors };\n\n if (!result.isValid && Object.keys(result.errors).length > 0) {\n // Set new errors\n Object.assign(newErrors, result.errors);\n } else {\n // Clear errors for this field if validation passed\n if (newErrors[fieldName]) {\n delete newErrors[fieldName];\n }\n }\n\n const isFormValid = Object.values(newErrors).every(\n (error) => !error\n );\n\n return {\n ...prev,\n errors: newErrors,\n isValid: isFormValid,\n };\n });\n }\n },\n onBlur: async (_e: React.FocusEvent<any>) => {\n setFormState((prev) => {\n const newTouched = name.includes(\".\")\n ? setNestedValue(prev.touched, name, true)\n : { ...prev.touched, [name]: true };\n return { ...prev, touched: newTouched };\n });\n\n if (validationManager.shouldValidate(\"onBlur\")) {\n const currentState = formStateRef.current!;\n const result = await validationManager.validateField(\n fieldName,\n currentState.values[fieldName],\n currentState.values,\n \"onBlur\"\n );\n if (!result.isValid) {\n setFormState((prev) => ({\n ...prev,\n errors: { ...prev.errors, ...result.errors },\n isValid: false,\n }));\n }\n }\n },\n };\n\n // Return different props for file inputs based on the current value type\n const currentValue = getNestedValue(formState.values, name);\n if (\n currentValue instanceof File ||\n currentValue instanceof FileList ||\n (Array.isArray(currentValue) &&\n currentValue.length > 0 &&\n currentValue[0] instanceof File)\n ) {\n return {\n ...baseProps,\n files: currentValue,\n };\n }\n\n return isCheckbox\n ? { ...baseProps, checked: Boolean(fieldValue) }\n : { ...baseProps, value: fieldValue || \"\" };\n },\n [\n defaultValues,\n dirtyManager,\n validationManager,\n fileValidators,\n formState.values,\n ]\n );\n\n // Provide typed overload surface for consumers\n const register = registerImpl as unknown as {\n <Name extends Path<T>>(name: Name): RegisterReturn<PathValue<T, Name>>;\n };\n\n // File management methods\n const addFile = useCallback(\n (name: string, file: File) => {\n const currentValue = getNestedValue(formState.values, name);\n\n if (currentValue instanceof FileList || Array.isArray(currentValue)) {\n // Add to existing files\n const newFiles = [...Array.from(currentValue), file];\n formStateManager.setValue(name, newFiles);\n } else {\n // Replace single file or set new file\n formStateManager.setValue(name, file);\n }\n },\n [formStateManager, formState.values]\n );\n\n const removeFile = useCallback(\n (name: string, index?: number) => {\n const currentValue = getNestedValue(formState.values, name);\n\n if (\n typeof index === \"number\" &&\n (currentValue instanceof FileList || Array.isArray(currentValue))\n ) {\n // Remove specific file by index\n const files = Array.from(currentValue);\n files.splice(index, 1);\n formStateManager.setValue(name, files);\n } else {\n // Clear all files\n formStateManager.setValue(name, null);\n }\n },\n [formStateManager, formState.values]\n );\n\n const clearFiles = useCallback(\n (name: string) => {\n formStateManager.setValue(name, null);\n },\n [formStateManager]\n );\n\n // Reset form - simplified with dirty manager\n const reset = useCallback(\n (options?: ResetOptions<T>) => {\n const newValues = options?.values ?? defaultValues;\n\n if (!options?.keepDirty) {\n dirtyManager.clearDirtyState();\n }\n\n setFormState({\n values: newValues,\n errors: options?.keepErrors ? formState.errors : {},\n touched: options?.keepTouched ? formState.touched : {},\n isSubmitting: false,\n isValid: false,\n isDirty: options?.keepDirty ? formState.isDirty : false,\n });\n\n // Clear file previews on reset\n setFilePreview({});\n },\n [defaultValues, formState, dirtyManager]\n );\n\n // Return the complete UseFormReturn interface - clean and modular!\n return {\n register,\n handleSubmit: submitOperations.handleSubmit,\n formState,\n reset,\n setValue: formStateManager.setValue,\n setValues: formStateManager.setValues,\n watch: formStateManager.watch,\n resetValues: formStateManager.resetValues,\n getFieldState: fieldOperations.getFieldState,\n isDirty: fieldOperations.isDirty,\n getDirtyFields: fieldOperations.getDirtyFields,\n getTouchedFields: fieldOperations.getTouchedFields,\n isFieldDirty: fieldOperations.isFieldDirty,\n isFieldTouched: fieldOperations.isFieldTouched,\n isFieldValid: fieldOperations.isFieldValid,\n hasErrors: fieldOperations.hasErrors,\n getErrorCount: fieldOperations.getErrorCount,\n markAllTouched: fieldOperations.markAllTouched,\n markFieldTouched: fieldOperations.markFieldTouched,\n markFieldUntouched: fieldOperations.markFieldUntouched,\n trigger: errorManagement.trigger,\n clearErrors: errorManagement.clearErrors,\n setError: errorManagement.setError,\n setFocus: focusManager.setFocus,\n addArrayItem: arrayOperations.addArrayItem,\n removeArrayItem: arrayOperations.removeArrayItem,\n resetField: fieldOperations.resetField,\n submit: submitOperations.submit,\n submitAsync: submitOperations.submitAsync,\n canSubmit,\n getSnapshot: formHistory.getSnapshot,\n restoreSnapshot: formHistory.restoreSnapshot,\n hasChanges: formHistory.hasChanges,\n getChanges: formHistory.getChanges,\n // File-specific methods\n addFile,\n removeFile,\n clearFiles,\n getFileInfo,\n getFilePreview,\n filePreview,\n };\n}\n","// React-specific utility functions\nimport {\n setNestedValue,\n getNestedValue,\n removeArrayItem,\n parseZodErrors,\n} from \"el-form-core\";\n\n// Re-export core utilities for convenience\nexport { setNestedValue, getNestedValue, removeArrayItem, parseZodErrors };\n\n// Re-export array helpers\nexport * from \"./arrayHelpers\";\n\n/**\n * Efficient equality comparison utilities\n */\nexport * from \"./equality\";\n\n/**\n * Dirty state management utilities\n */\nexport * from \"./dirtyState\";\n\n/**\n * Validation management utilities\n */\nexport * from \"./validation\";\n\n/**\n * Field operations utilities\n */\nexport * from \"./fieldOperations\";\n\n/**\n * Form state management utilities\n */\nexport * from \"./formState\";\n\n/**\n * Submit operations utilities\n */\nexport * from \"./submitOperations\";\n\n/**\n * Error management utilities\n */\nexport * from \"./errorManagement\";\n\n/**\n * Form history utilities\n */\nexport * from \"./formHistory\";\n\n/**\n * Focus management utilities\n */\nexport * from \"./focusManagement\";\n\n/**\n * Array operations utilities\n */\nexport * from \"./arrayOperations\";\n\n/**\n * File utilities\n */\nexport * from \"./fileUtils\";\n","// React-specific array manipulation (different name to avoid conflicts)\nexport function addArrayItemReact(obj: any, path: string, item: any): any {\n const result = { ...obj };\n\n // Handle array notation like employees[0].friends\n const normalizedPath = path.replace(/\\[(\\d+)\\]/g, \".$1\");\n const keys = normalizedPath.split(\".\").filter((key) => key !== \"\");\n let current = result;\n\n // Navigate to the parent object\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n\n if (!isNaN(Number(key))) {\n // Array index\n if (Array.isArray(current)) {\n current[Number(key)] = Array.isArray(current[Number(key)])\n ? [...current[Number(key)]]\n : { ...current[Number(key)] };\n current = current[Number(key)];\n }\n } else {\n // Object key\n if (typeof current[key] !== \"object\" || current[key] === null) {\n current[key] = {};\n } else {\n current[key] = Array.isArray(current[key])\n ? [...current[key]]\n : { ...current[key] };\n }\n current = current[key];\n }\n }\n\n const arrayKey = keys[keys.length - 1];\n\n if (!isNaN(Number(arrayKey))) {\n // Adding to an array at a numeric index (shouldn't happen with this function)\n if (Array.isArray(current)) {\n current = [...current];\n current[Number(arrayKey)] = item;\n }\n } else {\n // Adding to an array property\n if (!Array.isArray(current[arrayKey])) {\n current[arrayKey] = [];\n } else {\n current[arrayKey] = [...current[arrayKey]]; // Create new array\n }\n current[arrayKey].push(item); // Now safe to push to the new array\n }\n\n return result;\n}\n","/**\n * Efficient equality comparison utilities for form state management\n */\n\n/**\n * Shallow equality comparison for objects\n * Much faster than JSON.stringify for simple comparisons\n */\nexport function shallowEqual(obj1: any, obj2: any): boolean {\n if (obj1 === obj2) return true;\n if (obj1 == null || obj2 == null) return false;\n if (typeof obj1 !== \"object\" || typeof obj2 !== \"object\")\n return obj1 === obj2;\n\n const keys1 = Object.keys(obj1);\n const keys2 = Object.keys(obj2);\n\n if (keys1.length !== keys2.length) return false;\n\n for (let key of keys1) {\n if (!keys2.includes(key) || obj1[key] !== obj2[key]) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Deep equality comparison for complex nested objects\n * Only use when shallow comparison fails for performance\n */\nexport function deepEqual(obj1: any, obj2: any): boolean {\n if (obj1 === obj2) return true;\n if (obj1 == null || obj2 == null) return false;\n if (typeof obj1 !== typeof obj2) return false;\n\n if (typeof obj1 !== \"object\") return obj1 === obj2;\n\n if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;\n\n if (Array.isArray(obj1)) {\n if (obj1.length !== obj2.length) return false;\n for (let i = 0; i < obj1.length; i++) {\n if (!deepEqual(obj1[i], obj2[i])) return false;\n }\n return true;\n }\n\n const keys1 = Object.keys(obj1);\n const keys2 = Object.keys(obj2);\n\n if (keys1.length !== keys2.length) return false;\n\n for (let key of keys1) {\n if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {\n return false;\n }\n }\n return true;\n}\n","import { getNestedValue } from \"el-form-core\";\nimport { shallowEqual, deepEqual } from \"./equality\";\n\n/**\n * Dirty state management utilities for form fields\n * Provides efficient tracking of which fields have been modified\n */\n\nexport interface DirtyStateManager<T extends Record<string, any>> {\n dirtyFieldsRef: React.MutableRefObject<Set<string>>;\n checkIsDirty: (\n currentValues: Partial<T>,\n defaultValues: Partial<T>\n ) => boolean;\n checkFieldIsDirty: (\n fieldName: keyof T,\n currentValue: any,\n defaultValue: any\n ) => boolean;\n updateFieldDirtyState: (\n fieldName: string,\n value: any,\n defaultValues: Partial<T>\n ) => void;\n clearDirtyState: () => void;\n removeDirtyField: (fieldName: string) => void;\n addDirtyField: (fieldName: string) => void;\n}\n\n/**\n * Create a dirty state manager for efficient form state tracking\n */\nexport function createDirtyStateManager<T extends Record<string, any>>(\n dirtyFieldsRef: React.MutableRefObject<Set<string>>\n): DirtyStateManager<T> {\n return {\n dirtyFieldsRef,\n\n // Helper function to check if form is dirty (optimized)\n checkIsDirty: (\n currentValues: Partial<T>,\n defaultValues: Partial<T>\n ): boolean => {\n // Quick check: if we have tracked dirty fields, form is dirty\n if (dirtyFieldsRef.current.size > 0) return true;\n\n // Fallback: shallow comparison first, then deep if needed\n if (shallowEqual(defaultValues || {}, currentValues || {})) {\n return false;\n }\n\n // Only do deep comparison if shallow comparison fails\n return !deepEqual(defaultValues || {}, currentValues || {});\n },\n\n // Helper function to check if specific field is dirty (optimized)\n checkFieldIsDirty: (\n fieldName: keyof T,\n currentValue: any,\n defaultValue: any\n ): boolean => {\n const fieldKey = String(fieldName);\n\n // Quick check: if field is in dirty set, it's dirty\n if (dirtyFieldsRef.current.has(fieldKey)) return true;\n\n // Shallow comparison first\n if (defaultValue === currentValue) return false;\n\n // Deep comparison for complex values (arrays, objects)\n return !deepEqual(defaultValue, currentValue);\n },\n\n // Helper to mark field as dirty/clean\n updateFieldDirtyState: (\n fieldName: string,\n value: any,\n defaultValues: Partial<T>\n ): void => {\n const initialValue = getNestedValue(defaultValues || {}, fieldName);\n const isDirty = !deepEqual(initialValue, value);\n\n if (isDirty) {\n dirtyFieldsRef.current.add(fieldName);\n } else {\n dirtyFieldsRef.current.delete(fieldName);\n }\n },\n\n // Clear all dirty state\n clearDirtyState: (): void => {\n dirtyFieldsRef.current.clear();\n },\n\n // Remove specific field from dirty tracking\n removeDirtyField: (fieldName: string): void => {\n dirtyFieldsRef.current.delete(fieldName);\n },\n\n // Add field to dirty tracking\n addDirtyField: (fieldName: string): void => {\n dirtyFieldsRef.current.add(fieldName);\n },\n };\n}\n","import {\n ValidationEngine,\n ValidatorConfig,\n ValidatorEvent,\n getTypeName,\n getDiscriminatedUnionInfo,\n} from \"el-form-core\";\nimport type { z } from \"zod\";\n\n/**\n * Form validation utilities and managers\n * Handles field-level and form-level validation logic\n */\n\nexport interface ValidationManager<T extends Record<string, any>> {\n validateField: (\n fieldName: keyof T,\n fieldValue: any,\n formValues: Partial<T>,\n eventType: \"onChange\" | \"onBlur\" | \"onSubmit\"\n ) => Promise<{ isValid: boolean; errors: Record<string, string> }>;\n\n validateForm: (\n values: Partial<T>,\n eventType?: \"onChange\" | \"onBlur\" | \"onSubmit\"\n ) => Promise<{ isValid: boolean; errors: Record<keyof T, string> }>;\n\n shouldValidate: (eventType: \"onChange\" | \"onBlur\" | \"onSubmit\") => boolean;\n}\n\nexport interface ValidationManagerOptions<T extends Record<string, any>> {\n validationEngine: React.MutableRefObject<ValidationEngine>;\n validators: ValidatorConfig;\n fieldValidators: Partial<Record<keyof T, ValidatorConfig>>;\n mode: \"onChange\" | \"onBlur\" | \"onSubmit\" | \"all\";\n validateOn?: \"onChange\" | \"onBlur\" | \"onSubmit\" | \"manual\";\n schema?: z.ZodTypeAny;\n}\n\n/**\n * Create a validation manager for handling form and field validation\n */\nexport function createValidationManager<T extends Record<string, any>>(\n options: ValidationManagerOptions<T>\n): ValidationManager<T> {\n const {\n validationEngine,\n validators,\n fieldValidators,\n mode,\n validateOn,\n schema,\n } = options;\n\n return {\n // Determine if validation should run based on mode and validateOn option\n shouldValidate: (\n eventType: \"onChange\" | \"onBlur\" | \"onSubmit\"\n ): boolean => {\n // New validateOn option takes precedence\n if (validateOn) {\n if (validateOn === \"manual\") return false;\n if (validateOn === eventType) return true;\n if (eventType === \"onSubmit\") return true; // Always validate on submit\n return false;\n }\n\n // Smart validation: if validators has the specific event, enable it regardless of mode\n const hasValidatorForEvent = validators && (validators as any)[eventType];\n if (hasValidatorForEvent) {\n return true;\n }\n\n // Fallback to mode\n if (mode === \"all\") return true;\n if (mode === eventType) return true;\n if (eventType === \"onSubmit\") return true; // Always validate on submit\n\n return false;\n },\n\n // Validate field using the new validation system\n validateField: async (\n fieldName: keyof T,\n fieldValue: any,\n formValues: Partial<T>,\n eventType: \"onChange\" | \"onBlur\" | \"onSubmit\"\n ): Promise<{ isValid: boolean; errors: Record<string, string> }> => {\n const fieldKey = String(fieldName);\n const fieldConfig = (fieldValidators as any)[fieldKey];\n if (!fieldConfig && !validators) return { isValid: true, errors: {} };\n\n const event: ValidatorEvent = {\n type: eventType,\n isAsync: false,\n fieldName: String(fieldName),\n };\n\n let result = { isValid: true, errors: {} };\n\n // Validate with field-specific validators first\n if (fieldConfig) {\n result = await validationEngine.current.validateField(\n String(fieldName),\n fieldValue,\n formValues,\n fieldConfig,\n event\n );\n }\n\n // If field validation passes, run form-level validation\n if (result.isValid && validators) {\n const formResult = await validationEngine.current.validateForm(\n formValues,\n validators,\n event\n );\n\n // Extract any field-specific errors from form validation\n if (!formResult.isValid && formResult.errors[String(fieldName)]) {\n result = {\n isValid: false,\n errors: {\n [String(fieldName)]: formResult.errors[String(fieldName)],\n },\n };\n }\n }\n\n // Schema validation for discriminated unions\n if (result.isValid && schema) {\n try {\n // For discriminated union validation, we need to validate the entire form\n // against the schema when the discriminator field changes\n // const { getTypeName } = require(\"el-form-core\");\n if (getTypeName(schema) === \"ZodDiscriminatedUnion\") {\n const discriminatorField = String(fieldName);\n // If this is the discriminator field, validate the entire discriminated union\n // const { getDiscriminatedUnionInfo } = require(\"el-form-core\");\n const du = getDiscriminatedUnionInfo(schema);\n if (du && du.discriminator === discriminatorField) {\n // Validate the discriminated union\n const parseResult = schema.safeParse(formValues);\n if (!parseResult.success) {\n result = {\n isValid: false,\n errors: {\n [String(fieldName)]: parseResult.error.issues\n .filter((err) => err.path.includes(discriminatorField))\n .map((err) => err.message)\n .join(\", \"),\n },\n };\n }\n }\n }\n } catch (error: unknown) {\n // If schema validation fails, don't break the form\n console.warn(\"Schema validation error:\", error);\n }\n }\n\n return result;\n },\n\n // Validate entire form\n validateForm: async (\n values: Partial<T>,\n eventType: \"onChange\" | \"onBlur\" | \"onSubmit\" = \"onSubmit\"\n ): Promise<{ isValid: boolean; errors: Record<keyof T, string> }> => {\n const event: ValidatorEvent = {\n type: eventType,\n isAsync: false,\n };\n\n let allErrors: Record<string, string> = {};\n let isValid = true;\n\n // Validate all fields with field-level validators\n for (const [fieldName, fieldConfig] of Object.entries(fieldValidators)) {\n const fieldResult = await validationEngine.current.validateField(\n fieldName,\n values[fieldName as keyof T],\n values,\n fieldConfig as ValidatorConfig,\n event\n );\n\n if (!fieldResult.isValid) {\n isValid = false;\n Object.assign(allErrors, fieldResult.errors);\n }\n }\n\n // Run form-level validation\n if (validators) {\n const formResult = await validationEngine.current.validateForm(\n values,\n validators,\n event\n );\n\n if (!formResult.isValid) {\n isValid = false;\n Object.assign(allErrors, formResult.errors);\n }\n }\n\n // Schema validation\n if (schema && isValid) {\n try {\n const parseResult = schema.safeParse(values);\n if (!parseResult.success) {\n isValid = false;\n // Convert Zod errors to our error format\n parseResult.error.issues.forEach((err) => {\n const fieldName = err.path.join(\".\");\n if (!allErrors[fieldName]) {\n allErrors[fieldName] = err.message;\n }\n });\n }\n } catch (error: unknown) {\n console.warn(\"Schema validation error:\", error);\n }\n }\n\n return { isValid, errors: allErrors as Record<keyof T, string> };\n },\n };\n}\n","import { FormState, FieldState } from \"../types\";\nimport { DirtyStateManager } from \"./dirtyState\";\nimport { setNestedValue } from \"el-form-core\";\nimport { getNestedValue } from \"el-form-core\";\n\n/**\n * Field operations utilities for form state management\n * Handles field-level state checking and manipulation\n */\n\nexport interface FieldOperationsManager<T extends Record<string, any>> {\n isFieldDirty: (name: string) => boolean;\n isFieldTouched: (name: string) => boolean;\n isFieldValid: (name: string) => boolean;\n hasErrors: () => boolean;\n getErrorCount: () => number;\n markAllTouched: () => void;\n markFieldTouched: (name: string) => void;\n markFieldUntouched: (name: string) => void;\n getDirtyFields: () => Partial<Record<keyof T, boolean>>;\n getTouchedFields: () => Partial<Record<keyof T, boolean>>;\n resetField: <Name extends keyof T>(name: Name) => void;\n getFieldState: <Name extends keyof T>(name: Name) => FieldState;\n isDirty: <Name extends keyof T>(name?: Name) => boolean;\n}\n\nexport interface FieldOperationsOptions<T extends Record<string, any>> {\n formState: FormState<T>;\n setFormState: React.Dispatch<React.SetStateAction<FormState<T>>>;\n dirtyManager: DirtyStateManager<T>;\n defaultValues: Partial<T>;\n}\n\n/**\n * Create field operations manager for handling field-level state operations\n */\nexport function createFieldOperationsManager<T extends Record<string, any>>(\n options: FieldOperationsOptions<T>\n): FieldOperationsManager<T> {\n const { formState, setFormState, dirtyManager, defaultValues } = options;\n\n return {\n // Form State Utilities\n isFieldDirty: (name: string): boolean => {\n const curr = getNestedValue(formState.values, name);\n const def = getNestedValue(defaultValues as any, name);\n return dirtyManager.checkFieldIsDirty(name as any, curr, def);\n },\n\n isFieldTouched: (name: string): boolean => {\n const touched = getNestedValue(formState.touched as any, name);\n return Boolean(touched);\n },\n\n isFieldValid: (name: string): boolean => {\n const err = getNestedValue(formState.errors as any, name);\n return !err;\n },\n\n hasErrors: (): boolean => {\n return Object.keys(formState.errors).length > 0;\n },\n\n getErrorCount: (): number => {\n return Object.keys(formState.errors).length;\n },\n\n // Bulk operations\n markAllTouched: (): void => {\n setFormState((prev) => {\n const newTouched: Partial<Record<keyof T, boolean>> = {};\n Object.keys(prev.values).forEach((key) => {\n newTouched[key as keyof T] = true;\n });\n return { ...prev, touched: newTouched };\n });\n },\n\n markFieldTouched: (name: string): void => {\n setFormState((prev) => {\n const newTouched = name.includes(\".\")\n ? setNestedValue(prev.touched, name, true)\n : { ...prev.touched, [name]: true };\n return { ...prev, touched: newTouched };\n });\n },\n\n markFieldUntouched: (name: string): void => {\n setFormState((prev) => {\n const newTouched = name.includes(\".\")\n ? setNestedValue(prev.touched, name, false)\n : { ...prev.touched, [name]: false };\n return { ...prev, touched: newTouched };\n });\n },\n\n // Get all dirty fields\n getDirtyFields: (): Partial<Record<keyof T, boolean>> => {\n const dirtyFields: Partial<Record<keyof T, boolean>> = {};\n\n // Use the efficient tracking set first\n dirtyManager.dirtyFieldsRef.current.forEach((fieldName) => {\n dirtyFields[fieldName as keyof T] = true;\n });\n\n // Fallback: check any remaining fields that might not be tracked\n Object.keys(formState.values).forEach((key) => {\n const fieldName = key as keyof T;\n if (\n !dirtyFields[fieldName] &&\n dirtyManager.checkFieldIsDirty(\n fieldName,\n formState.values[fieldName],\n (defaultValues as any)[fieldName]\n )\n ) {\n dirtyFields[fieldName] = true;\n }\n });\n\n return dirtyFields;\n },\n\n // Get all touched fields\n getTouchedFields: (): Partial<Record<keyof T, boolean>> => {\n return { ...formState.touched };\n },\n\n resetField: <Name extends keyof T>(name: Name) => {\n // Remove field from dirty tracking\n dirtyManager.removeDirtyField(String(name));\n\n setFormState((prev) => {\n const newValues = { ...prev.values };\n (newValues as any)[name] = (defaultValues as any)[name];\n\n const newErrors = { ...prev.errors };\n delete newErrors[name];\n\n const newTouched = { ...prev.touched };\n delete newTouched[name];\n\n return {\n ...prev,\n values: newValues,\n errors: newErrors,\n touched: newTouched,\n isDirty: dirtyManager.dirtyFieldsRef.current.size > 0,\n };\n });\n },\n\n getFieldState: <Name extends keyof T>(name: Name): FieldState => ({\n isDirty: dirtyManager.checkFieldIsDirty(\n name,\n formState.values[name],\n (defaultValues as any)[name]\n ),\n isTouched: Boolean(formState.touched[name]),\n error: formState.errors[name],\n }),\n\n // Check if form/field is dirty\n isDirty: <Name extends keyof T>(name?: Name): boolean => {\n if (name) {\n return dirtyManager.checkFieldIsDirty(\n name,\n formState.values[name],\n (defaultValues as any)[name]\n );\n }\n return formState.isDirty;\n },\n };\n}\n","import { FormState, UseFormReturn } from \"../types\";\nimport { DirtyStateManager } from \"./dirtyState\";\nimport { setNestedValue } from \"el-form-core\";\nimport type { Path } from \"../types/path\";\nimport { getNestedValue } from \"el-form-core\";\n\n/**\n * Form state management utilities\n * Handles form value operations and watching\n */\n\nexport interface FormStateManager<T extends Record<string, any>> {\n setValue: (path: string, value: any) => void;\n setValues: (values: Partial<T>) => void;\n resetValues: (values?: Partial<T>) => void;\n watch: UseFormReturn<T>[\"watch\"];\n}\n\nexport interface FormStateOptions<T extends Record<string, any>> {\n formState: FormState<T>;\n setFormState: React.Dispatch<React.SetStateAction<FormState<T>>>;\n dirtyManager: DirtyStateManager<T>;\n defaultValues: Partial<T>;\n}\n\n/**\n * Create form state manager for handling form value operations\n */\nexport function createFormStateManager<T extends Record<string, any>>(\n options: FormStateOptions<T>\n): FormStateManager<T> {\n const { formState, setFormState, dirtyManager, defaultValues } = options;\n\n return {\n setValue: (path: string, value: any) => {\n dirtyManager.updateFieldDirtyState(path, value, defaultValues);\n setFormState((prev) => ({\n ...prev,\n values: setNestedValue(prev.values, path, value),\n isDirty: dirtyManager.dirtyFieldsRef.current.size > 0,\n }));\n },\n\n // setValues - Set multiple field values at once\n setValues: (values: Partial<T>) => {\n Object.entries(values).forEach(([path, value]) => {\n dirtyManager.updateFieldDirtyState(path, value, defaultValues);\n });\n\n setFormState((prev) => ({\n ...prev,\n values: { ...prev.values, ...values },\n isDirty: dirtyManager.dirtyFieldsRef.current.size > 0,\n }));\n },\n\n // resetValues - Reset form with new default values\n resetValues: (values?: Partial<T>) => {\n const newValues = values ?? defaultValues;\n\n // Clear dirty state since we're resetting\n dirtyManager.clearDirtyState();\n\n setFormState({\n values: newValues,\n errors: {},\n touched: {},\n isSubmitting: false,\n isValid: false,\n isDirty: false,\n });\n },\n\n watch: ((nameOrNames?: Path<T> | Path<T>[]) => {\n if (!nameOrNames) return formState.values;\n if (Array.isArray(nameOrNames)) {\n const entries = nameOrNames.map(\n (name) =>\n [name, getNestedValue(formState.values, name as any)] as const\n );\n return Object.fromEntries(entries) as any;\n }\n return getNestedValue(formState.values, nameOrNames as any) as any;\n }) as UseFormReturn<T>[\"watch\"],\n };\n}\n","import { FormState, UseFormOptions, UseFormReturn } from \"../types\";\nimport { ValidationManager } from \"./validation\";\n\n/**\n * Submit operations utilities\n * Handles form submission logic and validation\n */\n\nexport interface SubmitOperationsManager<T extends Record<string, any>> {\n handleSubmit: UseFormReturn<T>[\"handleSubmit\"];\n submit: () => Promise<void>;\n submitAsync: () => Promise<\n | { success: true; data: T }\n | { success: false; errors: Partial<Record<keyof T, string>> }\n >;\n}\n\nexport interface SubmitOperationsOptions<T extends Record<string, any>> {\n formState: FormState<T>;\n setFormState: React.Dispatch<React.SetStateAction<FormState<T>>>;\n validationManager: ValidationManager<T>;\n onSubmit?: UseFormOptions<T>[\"onSubmit\"];\n}\n\n/**\n * Create submit operations manager for handling form submission\n */\nexport function createSubmitOperationsManager<T extends Record<string, any>>(\n options: SubmitOperationsOptions<T>\n): SubmitOperationsManager<T> {\n const { formState, setFormState, validationManager, onSubmit } = options;\n\n return {\n // Handle submit - simplified\n handleSubmit: (\n onValid: (data: T) => void,\n onError?: (errors: Record<keyof T, string>) => void\n ) => {\n return async (e: React.FormEvent) => {\n e.preventDefault();\n setFormState((prev) => ({ ...prev, isSubmitting: true }));\n\n const { isValid, errors } = await validationManager.validateForm(\n formState.values\n );\n\n setFormState((prev) => ({\n ...prev,\n errors,\n isValid,\n isSubmitting: false,\n // Mark all fields with errors as touched so they display\n touched: !isValid\n ? {\n ...prev.touched,\n ...Object.keys(errors).reduce(\n (acc, field) => ({ ...acc, [field]: true }),\n {}\n ),\n }\n : prev.touched,\n }));\n\n if (isValid) {\n await onValid(formState.values as T);\n } else if (onError) {\n onError(errors);\n }\n };\n },\n\n // Advanced form control methods\n submit: async (): Promise<void> => {\n if (!onSubmit) {\n console.warn(\"useForm: No onSubmit handler provided for submit()\");\n return;\n }\n\n setFormState((prev) => ({ ...prev, isSubmitting: true }));\n\n try {\n // Always validate before submitting\n const { isValid, errors } = await validationManager.validateForm(\n formState.values\n );\n\n setFormState((prev) => ({\n ...prev,\n errors,\n isValid,\n }));\n\n if (isValid) {\n await onSubmit(formState.values as T);\n }\n } finally {\n setFormState((prev) => ({ ...prev, isSubmitting: false }));\n }\n },\n\n submitAsync: async (): Promise<\n | { success: true; data: T }\n | { success: false; errors: Partial<Record<keyof T, string>> }\n > => {\n setFormState((prev) => ({ ...prev, isSubmitting: true }));\n\n try {\n // Always validate before submitting\n const { isValid, errors } = await validationManager.validateForm(\n formState.values\n );\n\n setFormState((prev) => ({\n ...prev,\n errors,\n isValid,\n }));\n\n if (isValid) {\n // If onSubmit is provided, call it\n if (onSubmit) {\n await onSubmit(formState.values as T);\n }\n return { success: true, data: formState.values as T };\n } else {\n return { success: false, errors };\n }\n } finally {\n setFormState((prev) => ({ ...prev, isSubmitting: false }));\n }\n },\n };\n}\n","import { FormState, UseFormReturn } from \"../types\";\nimport { ValidationManager } from \"./validation\";\n\n/**\n * Error management utilities\n * Handles error operations and manual validation triggering\n */\n\nexport interface ErrorManagementManager<T extends Record<string, any>> {\n clearErrors: (name?: keyof T) => void;\n setError: <Name extends keyof T>(name: Name, error: string) => void;\n trigger: UseFormReturn<T>[\"trigger\"];\n}\n\nexport interface ErrorManagementOptions<T extends Record<string, any>> {\n formState: FormState<T>;\n setFormState: React.Dispatch<React.SetStateAction<FormState<T>>>;\n validationManager: ValidationManager<T>;\n}\n\n/**\n * Create error management manager for handling errors and validation\n */\nexport function createErrorManagementManager<T extends Record<string, any>>(\n options: ErrorManagementOptions<T>\n): ErrorManagementManager<T> {\n const { formState, setFormState, validationManager } = options;\n\n return {\n // Clear errors\n clearErrors: (name?: keyof T) => {\n setFormState((prev) => {\n if (name) {\n const newErrors = { ...prev.errors };\n delete newErrors[name];\n return { ...prev, errors: newErrors };\n }\n return { ...prev, errors: {} };\n });\n },\n\n // Set error\n setError: <Name extends keyof T>(name: Name, error: string) => {\n setFormState((prev) => ({\n ...prev,\n errors: {\n ...prev.errors,\n [name]: error,\n },\n isValid: false,\n }));\n },\n\n // Manual validation trigger\n trigger: (async (nameOrNames?: keyof T | (keyof T)[]) => {\n if (!nameOrNames) {\n // Validate all fields\n const { isValid } = await validationManager.validateForm(\n formState.values\n );\n return isValid;\n }\n\n if (Array.isArray(nameOrNames)) {\n // Validate multiple fields\n const results = await Promise.all(\n nameOrNames.map((name) =>\n validationManager.validateField(\n name,\n formState.values[name],\n formState.values,\n \"onSubmit\"\n )\n )\n );\n return results.every((result) => result.isValid);\n }\n\n // Validate single field\n const result = await validationManager.validateField(\n nameOrNames,\n formState.values[nameOrNames],\n formState.values,\n \"onSubmit\"\n );\n return result.isValid;\n }) as UseFormReturn<T>[\"trigger\"],\n };\n}\n","import { FormState, FormSnapshot } from \"../types\";\nimport { DirtyStateManager } from \"./dirtyState\";\nimport { getNestedValue, setNestedValue } from \"el-form-core\";\n\n/**\n * Form history and persistence utilities\n * Handles form snapshots and change tracking\n */\n\nexport interface FormHistoryManager<T extends Record<string, any>> {\n getSnapshot: () => FormSnapshot<T>;\n restoreSnapshot: (snapshot: FormSnapshot<T>) => void;\n hasChanges: () => boolean;\n getChanges: () => Partial<T>;\n}\n\nexport interface FormHistoryOptions<T extends Record<string, any>> {\n formState: FormState<T>;\n setFormState: React.Dispatch<React.SetStateAction<FormState<T>>>;\n dirtyManager: DirtyStateManager<T>;\n defaultValues: Partial<T>;\n}\n\n/**\n * Create form history manager for handling snapshots and change tracking\n */\nexport function createFormHistoryManager<T extends Record<string, any>>(\n options: FormHistoryOptions<T>\n): FormHistoryManager<T> {\n const { formState, setFormState, dirtyManager, defaultValues } = options;\n\n return {\n // Form History & Persistence methods\n getSnapshot: (): FormSnapshot<T> => {\n return {\n values: { ...formState.values },\n errors: { ...formState.errors },\n touched: { ...formState.touched },\n timestamp: Date.now(),\n isDirty: formState.isDirty,\n };\n },\n\n restoreSnapshot: (snapshot: FormSnapshot<T>) => {\n // Clear current dirty state tracking\n dirtyManager.clearDirtyState();\n\n // Recalculate dirty state based on restored values vs defaults\n Object.entries(snapshot.values).forEach(([path, value]) => {\n const defaultValue = getNestedValue(defaultValues, path);\n if (value !== defaultValue) {\n dirtyManager.updateFieldDirtyState(path, value, defaultValues);\n }\n });\n\n setFormState({\n values: { ...snapshot.values },\n errors: { ...snapshot.errors },\n touched: { ...snapshot.touched },\n isSubmitting: false,\n isValid: Object.keys(snapshot.errors).length === 0,\n isDirty:\n snapshot.isDirty || dirtyManager.dirtyFieldsRef.current.size > 0,\n });\n },\n\n hasChanges: (): boolean => {\n return formState.isDirty;\n },\n\n getChanges: (): Partial<T> => {\n const changes: Partial<T> = {};\n\n // Get all fields that are dirty\n dirtyManager.dirtyFieldsRef.current.forEach((fieldPath) => {\n const currentVa