UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

174 lines 7.4 kB
import { useCallback, useMemo, useState } from 'react'; /** * Manages form state, validation, and submission logic. * * @param options Configuration options including initial values and validation schema. * @returns Form state and handler functions. */ export const useFormValidation = (options) => { const { initialValues, validationSchema, validateOnChange = true, validateOnBlur = true, validateOnSubmit = true, } = options; const [values, setValuesState] = useState(initialValues); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState(() => Object.keys(initialValues).reduce((acc, key) => { acc[key] = false; return acc; }, {})); const [isSubmitting, setIsSubmitting] = useState(false); const validateFieldInternal = useCallback(async (fieldName, currentValues) => { const rules = validationSchema[fieldName]; if (!rules) return undefined; const fieldRules = Array.isArray(rules) ? rules : [rules]; let error = undefined; for (const rule of fieldRules) { error = rule(currentValues[fieldName], fieldName, currentValues); if (error) break; // Stop at the first error for this field } setErrors((prevErrors) => (Object.assign(Object.assign({}, prevErrors), { [fieldName]: error }))); return error; }, [validationSchema]); const validateFormInternal = useCallback(async (currentValues) => { const newErrors = {}; for (const fieldName of Object.keys(validationSchema)) { const fieldError = await validateFieldInternal(fieldName, currentValues); if (fieldError) { newErrors[fieldName] = fieldError; } } setErrors(newErrors); // Set all errors at once return newErrors; }, [validationSchema, validateFieldInternal]); const handleChange = useCallback((event) => { const { name, value, type } = event.target; let parsedValue = value; // Handle checkboxes if (type === 'checkbox' && event.target instanceof HTMLInputElement) { parsedValue = event.target.checked; } // Handle multi-select potentially if (type === 'select-multiple' && event.target instanceof HTMLSelectElement) { parsedValue = Array.from(event.target.selectedOptions).map((option) => option.value); } const fieldName = name; setValuesState((prevValues) => { const newValues = Object.assign(Object.assign({}, prevValues), { [fieldName]: parsedValue }); if (validateOnChange) { validateFieldInternal(fieldName, newValues); } return newValues; }); }, [validateOnChange, validateFieldInternal]); const handleBlur = useCallback((event) => { const { name } = event.target; const fieldName = name; setTouched((prevTouched) => (Object.assign(Object.assign({}, prevTouched), { [fieldName]: true }))); if (validateOnBlur) { setValuesState((currentVals) => { // Ensure validation runs with the latest state validateFieldInternal(fieldName, currentVals); return currentVals; // No state change needed here }); } }, [validateOnBlur, validateFieldInternal]); const setFieldValue = useCallback((fieldName, value) => { setValuesState((prevValues) => { const newValues = Object.assign(Object.assign({}, prevValues), { [fieldName]: value }); if (validateOnChange) { // Optionally delay validation slightly if needed for rapid programmatic changes validateFieldInternal(fieldName, newValues); } return newValues; }); // Consider if programmatic set should also trigger touched // setTouched(prev => ({ ...prev, [fieldName]: true })); }, [validateOnChange, validateFieldInternal]); const setValues = useCallback((newValues) => { setValuesState(newValues); // Maybe re-validate all or clear errors? setErrors({}); setTouched(Object.keys(newValues).reduce((acc, key) => { acc[key] = false; return acc; }, {})); }, []); const setFieldTouched = useCallback((fieldName, isTouched) => { setTouched((prevTouched) => (Object.assign(Object.assign({}, prevTouched), { [fieldName]: isTouched }))); if (isTouched && validateOnBlur) { setValuesState((currentVals) => { // Ensure validation runs with the latest state validateFieldInternal(fieldName, currentVals); return currentVals; // No state change needed here }); } }, [validateOnBlur, validateFieldInternal]); const setAllTouched = useCallback((isTouched) => { setTouched((prevTouched) => { const newTouched = Object.assign({}, prevTouched); Object.keys(newTouched).forEach((key) => { newTouched[key] = isTouched; }); return newTouched; }); if (isTouched) { // Validate all fields when setting all to touched (e.g., on submit failure) validateFormInternal(values); } }, [validateFormInternal, values]); const handleSubmit = useCallback((onSubmit) => async (event) => { event === null || event === void 0 ? void 0 : event.preventDefault(); setIsSubmitting(true); setAllTouched(true); // Mark all fields as touched let formErrors = {}; if (validateOnSubmit) { formErrors = await validateFormInternal(values); } const hasFormErrors = Object.values(formErrors).some((error) => !!error); if (!hasFormErrors) { try { await onSubmit(values); } catch (submitError) { console.error('Form submission error:', submitError); // Optionally set a form-level error // setErrors(prev => ({ ...prev, _form: 'Submission failed' })); } finally { setIsSubmitting(false); } } else { setIsSubmitting(false); // Focus the first field with an error (optional, requires element refs) } }, [validateOnSubmit, validateFormInternal, values, setAllTouched]); const resetForm = useCallback(() => { setValuesState(initialValues); setErrors({}); setTouched(Object.keys(initialValues).reduce((acc, key) => { acc[key] = false; return acc; }, {})); setIsSubmitting(false); }, [initialValues]); const hasErrors = useMemo(() => Object.values(errors).some((error) => !!error), [errors]); return { values, errors, hasErrors, touched, isSubmitting, handleChange, handleBlur, handleSubmit, setFieldValue, setValues, setFieldTouched, setAllTouched, validateField: (fieldName) => validateFieldInternal(fieldName, values), validateForm: () => validateFormInternal(values), resetForm, }; }; //# sourceMappingURL=useFormValidation.js.map