diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-03 19:34:16 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | ddd45e29745b73e7fe1684e197dcff598b375644 (patch) | |
| tree | 8bf01305b5c0d163c52a7dce747ed7a4a4650acb /src/utils | |
| parent | 5d3e8a4d0c2ce2ad8f22df857ab3ce54fcfc38ac (diff) | |
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
Diffstat (limited to 'src/utils')
| -rw-r--r-- | src/utils/hooks/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/index.ts | 3 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form-submit/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form-submit/use-form-submit.test.ts | 163 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form-submit/use-form-submit.ts | 122 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form-values/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form-values/use-form-values.test.ts | 69 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form-values/use-form-values.ts | 69 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form.test.ts | 225 | ||||
| -rw-r--r-- | src/utils/hooks/use-form/use-form.ts | 134 |
10 files changed, 788 insertions, 0 deletions
diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 9181b6a..9cc2b0f 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -5,6 +5,7 @@ export * from './use-boolean'; export * from './use-breadcrumb'; export * from './use-comments'; export * from './use-data-from-api'; +export * from './use-form'; export * from './use-github-api'; export * from './use-headings-tree'; export * from './use-is-mounted'; diff --git a/src/utils/hooks/use-form/index.ts b/src/utils/hooks/use-form/index.ts new file mode 100644 index 0000000..9febc8b --- /dev/null +++ b/src/utils/hooks/use-form/index.ts @@ -0,0 +1,3 @@ +export * from './use-form'; +export * from './use-form-submit'; +export * from './use-form-values'; diff --git a/src/utils/hooks/use-form/use-form-submit/index.ts b/src/utils/hooks/use-form/use-form-submit/index.ts new file mode 100644 index 0000000..f7f5bdf --- /dev/null +++ b/src/utils/hooks/use-form/use-form-submit/index.ts @@ -0,0 +1 @@ +export * from './use-form-submit'; diff --git a/src/utils/hooks/use-form/use-form-submit/use-form-submit.test.ts b/src/utils/hooks/use-form/use-form-submit/use-form-submit.test.ts new file mode 100644 index 0000000..cb0da16 --- /dev/null +++ b/src/utils/hooks/use-form/use-form-submit/use-form-submit.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import type { FormEvent } from 'react'; +import { type FormSubmitValidation, useFormSubmit } from './use-form-submit'; + +const generateSubmitEvent = () => + new Event('submit', { + bubbles: true, + cancelable: true, + }) as unknown as FormEvent<HTMLFormElement>; + +describe('useFormSubmit', () => { + const data = { foo: 'tempore', bar: false, baz: 42 }; + const messages = { error: 'Error', success: 'Success' }; + + it('can submit the provided data', async () => { + const { result } = renderHook(() => useFormSubmit(data)); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(4); + + expect(result.current.messages).toBeNull(); + expect(result.current.submitStatus).toBe('IDLE'); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.messages).toBeNull(); + expect(result.current.submitStatus).toBe('SUCCEEDED'); + }); + + it('can use a callback to handle submit', async () => { + const callback = jest.fn((_data) => undefined); + const { result } = renderHook(() => + useFormSubmit(data, { submit: callback }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(5); + + expect(callback).not.toHaveBeenCalled(); + expect(result.current.messages).toBeNull(); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(data); + expect(result.current.messages).toBeNull(); + }); + + it('can use a callback that fails validating data on submit', async () => { + const callback = jest.fn( + (values: typeof data): FormSubmitValidation<typeof data> => { + return { + messages, + validator: () => values.bar, + }; + } + ); + const { result } = renderHook(() => + useFormSubmit(data, { submit: callback }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(6); + + expect(callback).not.toHaveBeenCalled(); + expect(result.current.messages).toBeNull(); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(data); + expect(result.current.submitStatus).toBe('FAILED'); + expect(result.current.messages).toBe(messages); + }); + + it('can use a callback that succeeds validating data on submit', async () => { + const callback = jest.fn( + (values: typeof data): FormSubmitValidation<typeof data> => { + return { + messages, + validator: () => !values.bar, + }; + } + ); + const { result } = renderHook(() => + useFormSubmit(data, { submit: callback }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(6); + + expect(callback).not.toHaveBeenCalled(); + expect(result.current.messages).toBeNull(); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(data); + expect(result.current.submitStatus).toBe('SUCCEEDED'); + expect(result.current.messages).toBe(messages); + }); + + it('can call an onSuccess callback on success', async () => { + const callback = jest.fn(); + const { result } = renderHook(() => + useFormSubmit(data, { onSuccess: callback }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(6); + + expect(callback).not.toHaveBeenCalled(); + expect(result.current.messages).toBeNull(); + expect(result.current.submitStatus).toBe('IDLE'); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.messages).toBeNull(); + expect(callback).toHaveBeenCalledTimes(1); + expect(result.current.submitStatus).toBe('SUCCEEDED'); + }); + + it('can call an onFailure callback on failure', async () => { + const handlers = { + onFailure: jest.fn(), + submit: jest.fn( + (values: typeof data): FormSubmitValidation<typeof data> => { + return { + messages, + validator: () => values.bar, + }; + } + ), + }; + const { result } = renderHook(() => useFormSubmit(data, handlers)); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(6); + + expect(handlers.onFailure).not.toHaveBeenCalled(); + expect(result.current.messages).toBeNull(); + expect(result.current.submitStatus).toBe('IDLE'); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.messages).toBe(messages); + expect(handlers.onFailure).toHaveBeenCalledTimes(1); + expect(result.current.submitStatus).toBe('FAILED'); + }); +}); diff --git a/src/utils/hooks/use-form/use-form-submit/use-form-submit.ts b/src/utils/hooks/use-form/use-form-submit/use-form-submit.ts new file mode 100644 index 0000000..8d02395 --- /dev/null +++ b/src/utils/hooks/use-form/use-form-submit/use-form-submit.ts @@ -0,0 +1,122 @@ +import { useCallback, useState, type FormEvent } from 'react'; +import type { DataValidator, Maybe, Nullable } from '../../../../types'; + +export type FormSubmitMessages = { + /** + * The message to use on error. + */ + error: string; + /** + * The message to use on success. + */ + success: string; +}; + +export type FormSubmitValidation<T> = { + /** + * A callback to handle submit validation. + */ + validator: DataValidator<T>; + /** + * The messages to use on failure or success. + */ + messages: Partial<FormSubmitMessages>; +}; + +export type FormSubmitHandler<T> = ( + data: T +) => Maybe<FormSubmitValidation<T>> | Promise<Maybe<FormSubmitValidation<T>>>; + +export type FormSubmitStatus = 'IDLE' | 'PENDING' | 'FAILED' | 'SUCCEEDED'; + +export type FormHandlers<T extends Record<string, unknown>> = { + /** + * A callback function to handle submit failure. + */ + onFailure: () => void; + /** + * A callback function to handle submit success. + */ + onSuccess: () => void; + /** + * A callback function to handle submit. + */ + submit: FormSubmitHandler<T>; +}; + +export type UseFormSubmitReturn = { + /** + * The message to use on submit failure or success. + */ + messages: Nullable<Partial<FormSubmitMessages>>; + /** + * A method to handle form submit. + * + * @param {FormEvent<HTMLFormElement>} e - The event. + * @returns {Promise<void>} + */ + submit: (e: FormEvent<HTMLFormElement>) => Promise<void>; + /** + * The submit status. + */ + submitStatus: FormSubmitStatus; +}; + +/** + * React hook to handle form submit. + * + * @template {object} T - The object keys should match the fields name. + * @param {T} data - The form values. + * @param {Partial<FormHandlers<T>>} handlers - The submit handlers. + * @returns {UseFormSubmitReturn} A submit method, the status and messages. + */ +export const useFormSubmit = <T extends Record<string, unknown>>( + data: T, + handlers?: Partial<FormHandlers<T>> +): UseFormSubmitReturn => { + const { onFailure, onSuccess, submit: submitHandler } = handlers ?? {}; + const [messages, setMessages] = + useState<Nullable<Partial<FormSubmitMessages>>>(null); + const [submitStatus, setSubmitStatus] = useState<FormSubmitStatus>('IDLE'); + + const handleFailure = useCallback(() => { + setSubmitStatus('FAILED'); + if (onFailure) onFailure(); + }, [onFailure]); + + const handleSuccess = useCallback(() => { + setSubmitStatus('SUCCEEDED'); + if (onSuccess) onSuccess(); + }, [onSuccess]); + + const handleSubmit = useCallback(async () => { + const submitResult = submitHandler ? await submitHandler(data) : undefined; + + if (!submitResult) { + handleSuccess(); + return; + } + + setMessages(submitResult.messages); + + const isSuccess = submitResult.validator(data); + + setSubmitStatus(isSuccess ? 'SUCCEEDED' : 'FAILED'); + + if (isSuccess) handleSuccess(); + else handleFailure(); + }, [data, handleFailure, handleSuccess, submitHandler]); + + const submit = useCallback( + async (e: FormEvent<HTMLFormElement>) => { + e.preventDefault(); + setMessages(null); + setSubmitStatus('PENDING'); + + return handleSubmit(); + }, + [handleSubmit] + ); + + return { messages, submit, submitStatus }; +}; diff --git a/src/utils/hooks/use-form/use-form-values/index.ts b/src/utils/hooks/use-form/use-form-values/index.ts new file mode 100644 index 0000000..664a862 --- /dev/null +++ b/src/utils/hooks/use-form/use-form-values/index.ts @@ -0,0 +1 @@ +export * from './use-form-values'; diff --git a/src/utils/hooks/use-form/use-form-values/use-form-values.test.ts b/src/utils/hooks/use-form/use-form-values/use-form-values.test.ts new file mode 100644 index 0000000..f86d910 --- /dev/null +++ b/src/utils/hooks/use-form/use-form-values/use-form-values.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import type { ChangeEvent } from 'react'; +import { useFormValues } from './use-form-values'; + +/** + * Generate a new change event. + * + * @param {string} name - The field name. + * @param {unknown} value - The new value of the field. + * @returns {ChangeEvent<HTMLInputElement>} The event. + */ +const generateChangeEvent = (name: string, value: unknown, type = 'text') => { + const ev = new Event('change'); + Object.defineProperty(ev, 'target', { + value: { + checked: type === 'checkbox' || type === 'radio' ? value : undefined, + name, + type, + value: type === 'checkbox' || type === 'radio' ? undefined : value, + }, + writable: false, + }); + + return ev as unknown as ChangeEvent<HTMLInputElement>; +}; + +describe('useFormValues', () => { + const initialValues = { + foo: 'hello', + bar: false, + }; + const newValues = { + foo: 'world', + bar: true, + }; + + it('can initialize the values', () => { + const { result } = renderHook(() => useFormValues(initialValues)); + + expect(result.current.values.bar).toBe(initialValues.bar); + expect(result.current.values.foo).toBe(initialValues.foo); + }); + + it('can update and reset the values', () => { + const { result } = renderHook(() => useFormValues(initialValues)); + + act(() => { + result.current.update( + generateChangeEvent('bar', newValues.bar, 'checkbox') + ); + }); + + expect(result.current.values.bar).toBe(newValues.bar); + + act(() => { + result.current.update(generateChangeEvent('foo', newValues.foo)); + }); + + expect(result.current.values.foo).toBe(newValues.foo); + + act(() => { + result.current.reset(); + }); + + expect(result.current.values.bar).toBe(initialValues.bar); + expect(result.current.values.foo).toBe(initialValues.foo); + }); +}); diff --git a/src/utils/hooks/use-form/use-form-values/use-form-values.ts b/src/utils/hooks/use-form/use-form-values/use-form-values.ts new file mode 100644 index 0000000..8a0962f --- /dev/null +++ b/src/utils/hooks/use-form/use-form-values/use-form-values.ts @@ -0,0 +1,69 @@ +import { + type ChangeEventHandler, + useCallback, + useState, + type ChangeEvent, +} from 'react'; + +const isBooleanField = ( + target: EventTarget & (HTMLInputElement | HTMLTextAreaElement) +): target is EventTarget & HTMLInputElement => + target.type === 'checkbox' || target.type === 'radio'; + +export type UseFormValuesReturn<T extends Record<string, unknown>> = { + /** + * A method to reset the fields to their initial values. + * + * @returns {void} + */ + reset: () => void; + /** + * A method to handle input or textarea update. + * + * @param {ChangeEvent<HTMLTextAreaElement | HTMLInputElement>} e - The event. + * @returns {void} + */ + update: (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void; + /** + * The fields values. + */ + values: T; +}; + +/** + * React hook to handle form values update and reset. + * + * @template {object} T - The object keys should match the fields name. + * @param {T} initialValues - The fields initial values. + * @returns {UseFormValuesReturn<T>} An object with values and two methods. + */ +export const useFormValues = <T extends Record<string, unknown>>( + initialValues: T +): UseFormValuesReturn<T> => { + const [values, setValues] = useState(initialValues); + + /** + * Reset the field to their initial values. + */ + const reset = useCallback(() => { + setValues(initialValues); + }, [initialValues]); + + /** + * Handle input and textarea update. + * + * @param {ChangeEvent<HTMLTextAreaElement | HTMLInputElement>} e - The event. + * @returns {void} + */ + const update: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = + useCallback(({ target }) => { + setValues((prevData) => { + return { + ...prevData, + [target.name]: isBooleanField(target) ? target.checked : target.value, + }; + }); + }, []); + + return { values, reset, update }; +}; diff --git a/src/utils/hooks/use-form/use-form.test.ts b/src/utils/hooks/use-form/use-form.test.ts new file mode 100644 index 0000000..e2c30b5 --- /dev/null +++ b/src/utils/hooks/use-form/use-form.test.ts @@ -0,0 +1,225 @@ +import { describe, expect, it } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import type { ChangeEvent, FormEvent } from 'react'; +import { useForm } from './use-form'; + +const generateSubmitEvent = () => + new Event('submit', { + bubbles: true, + cancelable: true, + }) as unknown as FormEvent<HTMLFormElement>; + +/** + * Generate a new change event. + * + * @param {string} name - The field name. + * @param {unknown} value - The new value of the field. + * @returns {ChangeEvent<HTMLInputElement>} The event. + */ +const generateChangeEvent = (name: string, value: unknown, type = 'text') => { + const ev = new Event('change'); + Object.defineProperty(ev, 'target', { + value: { + checked: type === 'checkbox' || type === 'radio' ? value : undefined, + name, + type, + value: type === 'checkbox' || type === 'radio' ? undefined : value, + }, + writable: false, + }); + + return ev as unknown as ChangeEvent<HTMLInputElement>; +}; + +describe('useForm', () => { + it('can initialize the data', () => { + const initialValues = { + foo: 'impedit', + bar: 42, + }; + const { result } = renderHook(() => useForm({ initialValues })); + + expect(result.current.values.bar).toBe(initialValues.bar); + expect(result.current.values.foo).toBe(initialValues.foo); + }); + + it('can use a handler to validate the submit process', async () => { + const data = { + name: 'John', + }; + const submitHandler = jest.fn((_data) => undefined); + const { result } = renderHook(() => + useForm({ + initialValues: data, + submitHandler, + }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(3); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.validationErrors).toBeNull(); + expect(submitHandler).toHaveBeenCalled(); + expect(result.current.submitStatus).toBe('SUCCEEDED'); + }); + + it('can submit the data and reset to initial values', async () => { + const data = { + initial: { + name: 'John', + }, + new: { + name: 'Phoebe', + }, + }; + const { result } = renderHook(() => + useForm({ + initialValues: data.initial, + }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(3); + + act(() => { + result.current.update(generateChangeEvent('name', data.new.name)); + }); + + expect(result.current.values.name).toBe(data.new.name); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.submitStatus).toBe('SUCCEEDED'); + expect(result.current.values.name).toBe(data.initial.name); + }); + + it('can submit after validating the data', async () => { + const data = { + initial: { + name: 'John', + }, + new: { + name: 'Phoebe', + }, + errors: { + name: 'Expect name to have at least 2 letters.', + }, + }; + const submitHandler = jest.fn((_data) => undefined); + const { result } = renderHook(() => + useForm({ + initialValues: data.initial, + submitHandler, + validations: { + name: { + error: data.errors.name, + validator: (value) => value.length > 1, + }, + }, + }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(4); + + act(() => { + result.current.update(generateChangeEvent('name', data.new.name)); + }); + + expect(result.current.values.name).toBe(data.new.name); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.validationErrors).toBeNull(); + expect(submitHandler).toHaveBeenCalled(); + expect(result.current.submitStatus).toBe('SUCCEEDED'); + }); + + it('can abort submit if data validation fails', async () => { + const minAge = 18; + const data = { + initial: { + name: 'H', + age: 17, + }, + errors: { + age: `Expect age to be at least ${minAge}.`, + name: 'Expect name to have at least 2 letters.', + }, + }; + const submitHandler = jest.fn((_data) => undefined); + const { result } = renderHook(() => + useForm({ + initialValues: data.initial, + submitHandler, + validations: { + age: { + error: data.errors.age, + validator: (value) => value >= minAge, + }, + name: { + error: data.errors.name, + validator: (value) => value.length > 1, + }, + }, + }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(4); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.validationErrors?.age).toBe(data.errors.age); + expect(result.current.validationErrors?.name).toBe(data.errors.name); + expect(submitHandler).not.toHaveBeenCalled(); + expect(result.current.submitStatus).toBe('FAILED'); + }); + + it('can partially validate the data before submit', async () => { + const data = { + initial: { + name: 'H', + age: 17, + }, + errors: { + name: 'Expect name to have at least 2 letters.', + }, + }; + const submitHandler = jest.fn((_data) => undefined); + const { result } = renderHook(() => + useForm({ + initialValues: data.initial, + submitHandler, + validations: { + name: { + error: data.errors.name, + validator: (value) => value.length > 1, + }, + }, + }) + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(4); + + await act(async () => { + await result.current.submit(generateSubmitEvent()); + }); + + expect(result.current.validationErrors?.age).toBeUndefined(); + expect(result.current.validationErrors?.name).toBe(data.errors.name); + expect(submitHandler).not.toHaveBeenCalled(); + expect(result.current.submitStatus).toBe('FAILED'); + }); +}); 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<T> = { + /** + * The error message. + */ + error: string; + /** + * A function to validate the field value. + */ + validator: DataValidator<T>; +}; + +export type FormValidations<T> = { + [K in keyof T]: FormFieldValidation<T[K]>; +}; + +export type FormValidationErrors<T> = { + [K in keyof T]?: string; +}; + +export type UseFormConfig<T> = { + /** + * The initial fields values. + */ + initialValues: T; + /** + * A function to handle submit. + */ + submitHandler?: FormSubmitHandler<T>; + /** + * An object with validator and error message to validate each field. + */ + validations?: Partial<FormValidations<T>>; +}; + +export type UseFormReturn<T extends Record<string, unknown>> = + UseFormValuesReturn<T> & + UseFormSubmitReturn & { + /** + * The validation error for each field. + */ + validationErrors: Nullable<FormValidationErrors<T>>; + }; + +/** + * React hook to manage forms. + * + * @template {object} T - The object keys should match the fields name. + * @param {UseFormConfig<T>} config - The config. + * @returns {UseFormReturn<T>} The values, validations errors and form methods. + */ +export const useForm = <T extends Record<string, unknown>>({ + initialValues, + submitHandler, + validations, +}: UseFormConfig<T>): UseFormReturn<T> => { + const { values, reset, update } = useFormValues(initialValues); + const [validationErrors, setValidationErrors] = + useState<Nullable<FormValidationErrors<T>>>(null); + + const areValuesValid = useCallback(async () => { + if (!validations) return true; + + const keys = Object.keys(values) as Extract<keyof T, string>[]; + 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<T> = 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, + }; +}; |
