From ddd45e29745b73e7fe1684e197dcff598b375644 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 3 Nov 2023 19:34:16 +0100 Subject: feat(hooks): add an useForm hook * add two "sub"-hooks: useFormValues and useFormSubmit (that can be used independently) * handle initial data * handle custom submit callback * handle data validation * handle submit status --- src/utils/hooks/use-form/use-form.ts | 134 +++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/utils/hooks/use-form/use-form.ts (limited to 'src/utils/hooks/use-form/use-form.ts') diff --git a/src/utils/hooks/use-form/use-form.ts b/src/utils/hooks/use-form/use-form.ts new file mode 100644 index 0000000..492f820 --- /dev/null +++ b/src/utils/hooks/use-form/use-form.ts @@ -0,0 +1,134 @@ +import { useCallback, useState } from 'react'; +import type { DataValidator, Nullable } from '../../../types'; +import { + type FormSubmitHandler, + useFormSubmit, + type UseFormSubmitReturn, +} from './use-form-submit'; +import { type UseFormValuesReturn, useFormValues } from './use-form-values'; + +export type FormFieldValidation = { + /** + * The error message. + */ + error: string; + /** + * A function to validate the field value. + */ + validator: DataValidator; +}; + +export type FormValidations = { + [K in keyof T]: FormFieldValidation; +}; + +export type FormValidationErrors = { + [K in keyof T]?: string; +}; + +export type UseFormConfig = { + /** + * The initial fields values. + */ + initialValues: T; + /** + * A function to handle submit. + */ + submitHandler?: FormSubmitHandler; + /** + * An object with validator and error message to validate each field. + */ + validations?: Partial>; +}; + +export type UseFormReturn> = + UseFormValuesReturn & + UseFormSubmitReturn & { + /** + * The validation error for each field. + */ + validationErrors: Nullable>; + }; + +/** + * React hook to manage forms. + * + * @template {object} T - The object keys should match the fields name. + * @param {UseFormConfig} config - The config. + * @returns {UseFormReturn} The values, validations errors and form methods. + */ +export const useForm = >({ + initialValues, + submitHandler, + validations, +}: UseFormConfig): UseFormReturn => { + const { values, reset, update } = useFormValues(initialValues); + const [validationErrors, setValidationErrors] = + useState>>(null); + + const areValuesValid = useCallback(async () => { + if (!validations) return true; + + const keys = Object.keys(values) as Extract[]; + const validationPromises = keys.map(async (key) => { + const value = values[key]; + const field = validations[key]; + + if (!field) return true; + + const isValidField = await field.validator(value); + + if (!isValidField) + setValidationErrors((prevErr) => { + const newErrors: FormValidationErrors = prevErr + ? { ...prevErr } + : {}; + + return { + ...newErrors, + [key]: field.error, + }; + }); + + return isValidField; + }); + + const awaitedValidation = await Promise.all(validationPromises); + + return awaitedValidation.every((isValid) => isValid); + }, [validations, values]); + + const handleSubmit = useCallback(async () => { + setValidationErrors(null); + + const isValid = await areValuesValid(); + + if (isValid && submitHandler) return submitHandler(values); + + return { + validator: () => isValid, + messages: { + error: 'Has invalid values', + }, + }; + }, [areValuesValid, submitHandler, values]); + + const handleSuccess = useCallback(() => { + reset(); + }, [reset]); + + const { messages, submit, submitStatus } = useFormSubmit(values, { + onSuccess: handleSuccess, + submit: handleSubmit, + }); + + return { + messages, + reset, + submit, + submitStatus, + update, + values, + validationErrors, + }; +}; -- cgit v1.2.3