UNPKG

el-form-react

Version:

React form components and hooks powered by Zod validation

395 lines (392 loc) 11.1 kB
// src/hooks.ts export * from "el-form-core"; // src/useForm.ts import { useState, useCallback, useRef } from "react"; import { z } from "zod"; import { parseZodErrors as parseZodErrors2, setNestedValue as setNestedValue2, getNestedValue as getNestedValue2, removeArrayItem as removeArrayItem2 } from "el-form-core"; // src/utils/index.ts import { setNestedValue, getNestedValue, removeArrayItem, parseZodErrors } from "el-form-core"; function addArrayItemReact(obj, path, item) { const result = { ...obj }; const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1"); const keys = normalizedPath.split(".").filter((key) => key !== ""); let current = result; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!isNaN(Number(key))) { if (Array.isArray(current)) { current[Number(key)] = Array.isArray(current[Number(key)]) ? [...current[Number(key)]] : { ...current[Number(key)] }; current = current[Number(key)]; } } else { if (typeof current[key] !== "object" || current[key] === null) { current[key] = {}; } else { current[key] = Array.isArray(current[key]) ? [...current[key]] : { ...current[key] }; } current = current[key]; } } const arrayKey = keys[keys.length - 1]; if (!isNaN(Number(arrayKey))) { if (Array.isArray(current)) { current = [...current]; current[Number(arrayKey)] = item; } } else { if (!Array.isArray(current[arrayKey])) { current[arrayKey] = []; } else { current[arrayKey] = [...current[arrayKey]]; } current[arrayKey].push(item); } return result; } // src/useForm.ts function useForm(options) { const { schema, initialValues = {}, validateOnChange = false, validateOnBlur = false } = options; const fieldRefs = useRef(/* @__PURE__ */ new Map()); const [formState, setFormState] = useState({ values: initialValues, errors: {}, touched: {}, isSubmitting: false, isValid: false, isDirty: false }); const validate = useCallback( (values) => { try { schema.parse(values); return { isValid: true, errors: {} }; } catch (error) { if (error instanceof z.ZodError) { return { isValid: false, errors: parseZodErrors2(error) }; } return { isValid: false, errors: {} }; } }, [schema] ); const checkIsDirty = useCallback( (currentValues) => { return JSON.stringify(initialValues || {}) !== JSON.stringify(currentValues || {}); }, [initialValues] ); const checkFieldIsDirty = useCallback( (fieldName) => { const initialValue = initialValues[fieldName]; const currentValue = formState.values[fieldName]; return JSON.stringify(initialValue) !== JSON.stringify(currentValue); }, [initialValues, formState.values] ); const register = useCallback( (name) => { const fieldValue = name.includes(".") ? getNestedValue2(formState.values, name) : formState.values[name]; const isCheckbox = typeof fieldValue === "boolean"; const baseProps = { name, onChange: (e) => { const target = e.target; const value = target.type === "checkbox" ? target.checked : target.type === "number" ? target.value ? Number(target.value) : void 0 : target.value; setFormState((prev) => { const newValues = name.includes(".") ? setNestedValue2(prev.values, name, value) : { ...prev.values, [name]: value }; let newErrors = { ...prev.errors }; if (name.includes(".")) { const nestedError = getNestedValue2(newErrors, name); if (nestedError) { newErrors = setNestedValue2(newErrors, name, void 0); } } else { delete newErrors[name]; } if (validateOnChange) { const { errors } = validate(newValues); newErrors = errors; } return { ...prev, values: newValues, errors: newErrors, isDirty: checkIsDirty(newValues) }; }); }, onBlur: (_e) => { setFormState((prev) => { const newTouched = name.includes(".") ? setNestedValue2(prev.touched, name, true) : { ...prev.touched, [name]: true }; let newErrors = prev.errors; if (validateOnBlur) { const { errors } = validate(prev.values); newErrors = errors; } return { ...prev, touched: newTouched, errors: newErrors, isDirty: checkIsDirty(prev.values) }; }); } }; if (isCheckbox) { return { ...baseProps, checked: Boolean(fieldValue) }; } return { ...baseProps, value: fieldValue || "" }; }, [formState.values, validateOnChange, validateOnBlur, validate, checkIsDirty] ); const handleSubmit = useCallback( (onValid, onError) => { return (e) => { e.preventDefault(); setFormState((prev) => ({ ...prev, isSubmitting: true })); const { isValid, errors } = validate(formState.values); setFormState((prev) => ({ ...prev, errors, isValid, isSubmitting: false, isDirty: checkIsDirty(formState.values) })); if (isValid) { onValid(formState.values); } else { if (onError) { onError(errors); } } }; }, [formState.values, validate, checkIsDirty] ); const reset = useCallback( (options2) => { const newValues = options2?.values ?? initialValues; setFormState({ values: newValues, errors: options2?.keepErrors ? formState.errors : {}, touched: options2?.keepTouched ? formState.touched : {}, isSubmitting: false, isValid: false, isDirty: options2?.keepDirty ? formState.isDirty : false }); }, [initialValues, formState] ); const setValue = useCallback( (path, value) => { setFormState((prev) => { const newValues = setNestedValue2(prev.values, path, value); const { errors } = validate(newValues); return { ...prev, values: newValues, errors, isDirty: checkIsDirty(newValues) }; }); }, [validate, checkIsDirty] ); const addArrayItemHandler = useCallback( (path, item) => { setFormState((prev) => { const newValues = addArrayItemReact(prev.values, path, item); const { errors } = validate(newValues); return { ...prev, values: newValues, errors, isDirty: checkIsDirty(newValues) }; }); }, [validate, checkIsDirty] ); const removeArrayItemHandler = useCallback( (path, index) => { setFormState((prev) => { const newValues = removeArrayItem2(prev.values, path, index); const { errors } = validate(newValues); return { ...prev, values: newValues, errors, isDirty: checkIsDirty(newValues) }; }); }, [validate, checkIsDirty] ); const watch = useCallback( (nameOrNames) => { if (!nameOrNames) { return formState.values; } if (Array.isArray(nameOrNames)) { const result = {}; nameOrNames.forEach((name) => { result[name] = formState.values[name]; }); return result; } return formState.values[nameOrNames]; }, [formState.values] ); const isDirty = useCallback( (name) => { if (name) { return checkFieldIsDirty(name); } return formState.isDirty; }, [formState.isDirty, checkFieldIsDirty] ); const getFieldState = useCallback( (name) => { return { isDirty: checkFieldIsDirty(name), isTouched: !!formState.touched[name], error: formState.errors[name] }; }, [formState, checkFieldIsDirty] ); const getDirtyFields = useCallback(() => { const dirtyFields = {}; Object.keys(formState.values).forEach((key) => { const fieldName = key; if (checkFieldIsDirty(fieldName)) { dirtyFields[fieldName] = true; } }); return dirtyFields; }, [formState.values, checkFieldIsDirty]); const getTouchedFields = useCallback(() => { return { ...formState.touched }; }, [formState.touched]); const trigger = useCallback( async (nameOrNames) => { if (!nameOrNames) { const { isValid: isValid2 } = validate(formState.values); return isValid2; } if (Array.isArray(nameOrNames)) { const fieldsToValidate = {}; nameOrNames.forEach((name) => { fieldsToValidate[name] = formState.values[name]; }); const { isValid: isValid2 } = validate(fieldsToValidate); return isValid2; } const fieldToValidate = {}; fieldToValidate[nameOrNames] = formState.values[nameOrNames]; const { isValid } = validate(fieldToValidate); return isValid; }, [formState.values, validate] ); const clearErrors = useCallback((name) => { setFormState((prev) => { if (name) { const newErrors = { ...prev.errors }; delete newErrors[name]; return { ...prev, errors: newErrors }; } return { ...prev, errors: {} }; }); }, []); const setError = useCallback( (name, error) => { setFormState((prev) => ({ ...prev, errors: { ...prev.errors, [name]: error } })); }, [] ); const setFocus = useCallback( (name, options2) => { const fieldRef = fieldRefs.current.get(name); if (fieldRef) { fieldRef.focus(); if (options2?.shouldSelect && "select" in fieldRef) { fieldRef.select(); } } }, [] ); const resetField = useCallback( (name) => { setFormState((prev) => { const newValues = { ...prev.values }; newValues[name] = initialValues[name]; const newErrors = { ...prev.errors }; delete newErrors[name]; const newTouched = { ...prev.touched }; delete newTouched[name]; return { ...prev, values: newValues, errors: newErrors, touched: newTouched, isDirty: checkIsDirty(newValues) }; }); }, [initialValues, checkIsDirty] ); return { register, handleSubmit, formState, reset, setValue, watch, getFieldState, isDirty, getDirtyFields, getTouchedFields, trigger, clearErrors, setError, setFocus, addArrayItem: addArrayItemHandler, removeArrayItem: removeArrayItemHandler, resetField }; } export { useForm }; //# sourceMappingURL=hooks.mjs.map