@supunlakmal/hooks
Version:
A collection of reusable React hooks
174 lines • 7.4 kB
JavaScript
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