aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils/hooks/use-form/use-form-submit
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils/hooks/use-form/use-form-submit')
-rw-r--r--src/utils/hooks/use-form/use-form-submit/index.ts1
-rw-r--r--src/utils/hooks/use-form/use-form-submit/use-form-submit.test.ts163
-rw-r--r--src/utils/hooks/use-form/use-form-submit/use-form-submit.ts122
3 files changed, 286 insertions, 0 deletions
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 };
+};