Skip to content

Commit

Permalink
feat: global forms provider (#250)
Browse files Browse the repository at this point in the history
* feat: global forms provider

* fix: install react-test-renderer

* fix: use yarn 4

* fix: use act from @testing-library/react-hooks

* refactor: undo yarn changes as no package updates were done

* refactor: rename FormInfoObjectOrString

* docs: submit

* refactor: render hook options fn

* object.values

* refactor: use object functions

* refactor: make GlobalFormContext private

* fix: export

* Update src/GlobalFormsProvider/GlobalFormsContext.ts

Co-authored-by: Benedikt Franke <[email protected]>

* Update src/GlobalFormsProvider/GlobalFormsContext.ts

Co-authored-by: Benedikt Franke <[email protected]>

* Update src/GlobalFormsProvider/GlobalFormsProvider.tsx

Co-authored-by: Benedikt Franke <[email protected]>

* refactor: import maybe

---------

Co-authored-by: Benedikt Franke <[email protected]>
  • Loading branch information
mic-web and spawnia authored Sep 9, 2024
1 parent 2d8914f commit 5ddf2ca
Show file tree
Hide file tree
Showing 3 changed files with 405 additions and 0 deletions.
88 changes: 88 additions & 0 deletions src/GlobalFormsProvider/GlobalFormsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useCallback, useContext, useMemo } from 'react';

export type FormInfo = { name: string; order: number };
type FormInfoObjectOrString = FormInfo | string;

export type CallbackWithOrder = {
callback: () => Promise<void>;
order: number;
};

export type GlobalFormsContextType = {
isSubmitting: boolean;
reset: () => void;
setResetCallback: (form: FormInfo, callback: () => void) => void;
setSubmitCallback: (form: FormInfo, callback: () => Promise<void>) => void;
setSubmitting: (form: FormInfo, submitting: boolean) => void;
setSubmitSuccessful: (form: FormInfo, submitSuccessful: boolean) => void;
/** Returns true when all forms were submitted successfully. */
submit: (additionalCallback?: CallbackWithOrder) => Promise<boolean>;
};

export const GlobalFormsContext = React.createContext<GlobalFormsContextType>({
isSubmitting: false,
reset: () => {},
setResetCallback: () => {},
setSubmitCallback: () => {},
setSubmitting: () => {},
setSubmitSuccessful: () => {},
submit: () => Promise.resolve(false),
});

export function useGlobalForms() {
return useContext(GlobalFormsContext);
}

export function normalizeFormInfo(form: FormInfoObjectOrString): FormInfo {
if (typeof form === 'string') {
return { name: form, order: 0 };
}

return form;
}

export function useGlobalForm(form: FormInfoObjectOrString) {
const formInfo = useMemo(() => normalizeFormInfo(form), [form]);

const {
setResetCallback,
setSubmitCallback,
setSubmitting,
setSubmitSuccessful,
} = useContext(GlobalFormsContext);

const setResetCallbackWrapped = useCallback(
(callback: () => void) => {
setResetCallback(formInfo, callback);
},
[formInfo, setResetCallback],
);

const setSubmitCallbackWrapped = useCallback(
(callback: () => Promise<void>) => {
setSubmitCallback(formInfo, callback);
},
[formInfo, setSubmitCallback],
);

const setSubmittingWrapped = useCallback(
(submitting: boolean) => {
setSubmitting(formInfo, submitting);
},
[formInfo, setSubmitting],
);

const setSubmitSuccessfulWrapped = useCallback(
(submitSuccessful: boolean) => {
setSubmitSuccessful(formInfo, submitSuccessful);
},
[formInfo, setSubmitSuccessful],
);

return {
setResetCallback: setResetCallbackWrapped,
setSubmitCallback: setSubmitCallbackWrapped,
setSubmitSuccessful: setSubmitSuccessfulWrapped,
setSubmitting: setSubmittingWrapped,
};
}
179 changes: 179 additions & 0 deletions src/GlobalFormsProvider/GlobalFormsProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
act,
renderHook,
RenderHookOptions,
} from '@testing-library/react-hooks';
import React, { PropsWithChildren } from 'react';

import { useGlobalForms, useGlobalForm } from './GlobalFormsContext';
import { GlobalFormsProvider } from './GlobalFormsProvider';

function createRenderHookOptions(): RenderHookOptions<
PropsWithChildren<unknown>
> {
return {
wrapper: function Wrapper({ children }: PropsWithChildren<unknown>) {
return <GlobalFormsProvider>{children}</GlobalFormsProvider>;
},
};
}

describe('GlobalFormsProvider', () => {
it('finds one submitting form', () => {
const { result } = renderHook(() => {
const { isSubmitting } = useGlobalForms();
const { setSubmitting: setSubmitting1 } = useGlobalForm('form1');
const { setSubmitting: setSubmitting2 } = useGlobalForm('form2');

return { isSubmitting, setSubmitting1, setSubmitting2 };
}, createRenderHookOptions());
expect(result.error).toBeFalsy();
expect(result.current.isSubmitting).toBeFalsy();

act(() => result.current.setSubmitting1(true));
act(() => result.current.setSubmitting2(false));
expect(result.current.isSubmitting).toBeTruthy();
});

it('overrides submitting value', () => {
const { result } = renderHook(() => {
const { isSubmitting } = useGlobalForms();
const { setSubmitting } = useGlobalForm('form');

return { isSubmitting, setSubmitting };
}, createRenderHookOptions());
expect(result.current.isSubmitting).toBeFalsy();

act(() => result.current.setSubmitting(true));
expect(result.current.isSubmitting).toBeTruthy();

act(() => result.current.setSubmitting(false));
expect(result.current.isSubmitting).toBeFalsy();
});

it('calls all reset callbacks', () => {
const { result } = renderHook(() => {
const { reset } = useGlobalForms();
const { setResetCallback: setResetCallback1 } = useGlobalForm('form1');
const { setResetCallback: setResetCallback2 } = useGlobalForm('form2');

return { reset, setResetCallback1, setResetCallback2 };
}, createRenderHookOptions());
result.current.reset();

const reset1 = jest.fn();
act(() => result.current.setResetCallback1(reset1));

const reset2 = jest.fn();
act(() => result.current.setResetCallback2(reset2));

result.current.reset();
expect(reset1).toHaveBeenCalledTimes(1);
expect(reset2).toHaveBeenCalledTimes(1);

act(() => result.current.setResetCallback1(jest.fn()));
result.current.reset();
expect(reset1).toHaveBeenCalledTimes(1);
expect(reset2).toHaveBeenCalledTimes(2);
});

it('overrides reset callback', () => {
const { result } = renderHook(() => {
const { reset } = useGlobalForms();
const { setResetCallback } = useGlobalForm('form');

return { reset, setResetCallback };
}, createRenderHookOptions());
const resetOld = jest.fn();
act(() => result.current.setResetCallback(resetOld));
result.current.reset();
expect(resetOld).toHaveBeenCalledTimes(1);

const resetNew = jest.fn();
act(() => result.current.setResetCallback(resetNew));
result.current.reset();
expect(resetOld).toHaveBeenCalledTimes(1);
expect(resetNew).toHaveBeenCalledTimes(1);
});

it('calls all submit callbacks', async () => {
const { result } = renderHook(() => {
const { submit } = useGlobalForms();
const { setSubmitCallback: setSubmitCallback1 } = useGlobalForm('form1');
const { setSubmitCallback: setSubmitCallback2 } = useGlobalForm('form2');

return { submit, setSubmitCallback1, setSubmitCallback2 };
}, createRenderHookOptions());
await expect(result.current.submit()).resolves.toBeTruthy();

const submit1 = jest.fn(() => Promise.resolve());
act(() => result.current.setSubmitCallback1(submit1));

const submit2 = jest.fn(() => Promise.resolve());
act(() => result.current.setSubmitCallback2(submit2));

await expect(result.current.submit()).resolves.toBeFalsy();
expect(submit1).toHaveBeenCalledTimes(1);
expect(submit2).toHaveBeenCalledTimes(1);
});

it('overrides submit callback', async () => {
const { result } = renderHook(() => {
const { submit } = useGlobalForms();
const { setSubmitCallback } = useGlobalForm('form');

return { submit, setSubmitCallback };
}, createRenderHookOptions());

const submitOld = jest.fn(() => Promise.resolve());
act(() => result.current.setSubmitCallback(submitOld));

await expect(result.current.submit()).resolves.toBeFalsy();
expect(submitOld).toHaveBeenCalledTimes(1);

const submitNew = jest.fn(() => Promise.resolve());
act(() => result.current.setSubmitCallback(submitNew));

await expect(result.current.submit()).resolves.toBeFalsy();
expect(submitOld).toHaveBeenCalledTimes(1);
expect(submitNew).toHaveBeenCalledTimes(1);
});

it('maintains submit order', async () => {
const { result } = renderHook(() => {
const { submit } = useGlobalForms();
const { setSubmitCallback: setSubmitCallbackError } = useGlobalForm({
name: 'form-error',
order: 2,
});
const { setSubmitCallback: setSubmitCallbackFirst } = useGlobalForm({
name: 'form-first',
order: 1,
});
const { setSubmitCallback: setSubmitCallbackLast } = useGlobalForm({
name: 'form-last',
order: 3,
});

return {
submit,
setSubmitCallbackError,
setSubmitCallbackFirst,
setSubmitCallbackLast,
};
}, createRenderHookOptions());
const submitError = jest.fn(() => Promise.reject());
act(() => result.current.setSubmitCallbackError(submitError));

const submitFirst = jest.fn(() => Promise.resolve());
act(() => result.current.setSubmitCallbackFirst(submitFirst));

const submitLast = jest.fn(() => Promise.resolve());
act(() => result.current.setSubmitCallbackLast(submitLast));

await expect(result.current.submit()).rejects.toBeFalsy();
expect(submitFirst).toHaveBeenCalledTimes(1);
expect(submitError).toHaveBeenCalledTimes(1);
expect(submitLast).toHaveBeenCalledTimes(0);
});
});
Loading

0 comments on commit 5ddf2ca

Please sign in to comment.