From 9240ac884a720f560902d252b61b31949ea17f12 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 13:13:00 +0300 Subject: [PATCH 1/5] Created useBooleanField hook --- packages/x/src/useBooleanField.ts | 40 +++++++ packages/x/tests/useBooleanField.test.tsx | 125 ++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 packages/x/src/useBooleanField.ts create mode 100644 packages/x/tests/useBooleanField.test.tsx diff --git a/packages/x/src/useBooleanField.ts b/packages/x/src/useBooleanField.ts new file mode 100644 index 00000000..0c4fe548 --- /dev/null +++ b/packages/x/src/useBooleanField.ts @@ -0,0 +1,40 @@ +import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; + +export type BooleanFieldConfig = FieldConfig & { + required?: boolean; + requiredError?: string; +}; + +export const defaultRequiredError = 'Field is required'; + +export type BooleanFieldBag = FieldContext & { + onBlur: () => void; +}; + +export const useBooleanField = ({ required, requiredError = defaultRequiredError, ...config }: BooleanFieldConfig) => { + const fieldBag = useField(config); + + const { + control: { setTouched }, + } = fieldBag; + + const onBlur = () => { + setTouched({ $touched: true }); + }; + + useFieldValidator({ + name: config.name, + validator: (value) => { + if (required && !value) { + return requiredError; + } + + return undefined; + }, + }); + + return { + ...fieldBag, + onBlur, + }; +}; diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx new file mode 100644 index 00000000..03605b3a --- /dev/null +++ b/packages/x/tests/useBooleanField.test.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { BooleanFieldConfig, defaultRequiredError, useBooleanField } from '../src/useBooleanField'; + +type Config = Omit & { + initialValue?: boolean; +}; + +const renderUseStringField = (config: Config = {}) => { + const { initialValue = false, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const stringFieldBag = renderHook( + (props: Omit) => + useBooleanField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps, + }, + ); + + return [stringFieldBag, formBag] as const; +}; + +describe('Boolean field', () => { + it('Should set touched=true on blur', async () => { + const [{ result }] = renderUseStringField(); + + expect(result.current.meta.touched?.$touched).toBeFalsy(); + + await act(() => { + result.current.onBlur(); + }); + + await waitFor(() => { + expect(result.current.meta.touched?.$touched).toBeTruthy(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseStringField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + }); + + act(() => { + result.current.control.setValue(false); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + }); + + act(() => { + result.current.control.setValue(true); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseStringField({ required: true, requiredError: 'custom' }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(false); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(true); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); +}); From db2d6858f1bcab38c10eeef461f00e8909a7166f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 13:14:40 +0300 Subject: [PATCH 2/5] Added changeset --- .changeset/stale-cars-heal.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-cars-heal.md diff --git a/.changeset/stale-cars-heal.md b/.changeset/stale-cars-heal.md new file mode 100644 index 00000000..fea90e02 --- /dev/null +++ b/.changeset/stale-cars-heal.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': minor +--- + +Created useBooleanField hook From fe963a0292497c7bb57fe482991dd19d278d95f2 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:52:51 +0300 Subject: [PATCH 3/5] Refactoring --- packages/x/tests/useBooleanField.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx index 03605b3a..214f7386 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/x/tests/useBooleanField.test.tsx @@ -8,7 +8,7 @@ type Config = Omit & { initialValue?: boolean; }; -const renderUseStringField = (config: Config = {}) => { +const renderUseBooleanField = (config: Config = {}) => { const { initialValue = false, ...initialProps } = config; const formBag = renderHook(() => @@ -38,7 +38,7 @@ const renderUseStringField = (config: Config = {}) => { describe('Boolean field', () => { it('Should set touched=true on blur', async () => { - const [{ result }] = renderUseStringField(); + const [{ result }] = renderUseBooleanField(); expect(result.current.meta.touched?.$touched).toBeFalsy(); @@ -52,7 +52,7 @@ describe('Boolean field', () => { }); it('Should set default error if field is required and empty', async () => { - const [{ result }] = renderUseStringField({ required: true }); + const [{ result }] = renderUseBooleanField({ required: true }); act(() => { result.current.control.setValue(null); @@ -88,7 +88,7 @@ describe('Boolean field', () => { }); it('Should set custom error if field is required and empty', async () => { - const [{ result }] = renderUseStringField({ required: true, requiredError: 'custom' }); + const [{ result }] = renderUseBooleanField({ required: true, requiredError: 'custom' }); act(() => { result.current.control.setValue(null); From 330ad02aeb32a33fbb881e189c7ba94dca50e1b3 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:09:13 +0300 Subject: [PATCH 4/5] Refactored BooleanField custom errors API --- packages/x/src/useBooleanField.ts | 7 +++---- packages/x/tests/useBooleanField.test.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/x/src/useBooleanField.ts b/packages/x/src/useBooleanField.ts index 0c4fe548..309c73b3 100644 --- a/packages/x/src/useBooleanField.ts +++ b/packages/x/src/useBooleanField.ts @@ -1,8 +1,7 @@ import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; export type BooleanFieldConfig = FieldConfig & { - required?: boolean; - requiredError?: string; + required?: boolean | string; }; export const defaultRequiredError = 'Field is required'; @@ -11,7 +10,7 @@ export type BooleanFieldBag = FieldContext & { onBlur: () => void; }; -export const useBooleanField = ({ required, requiredError = defaultRequiredError, ...config }: BooleanFieldConfig) => { +export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => { const fieldBag = useField(config); const { @@ -26,7 +25,7 @@ export const useBooleanField = ({ required, requiredError = defaultRequiredError name: config.name, validator: (value) => { if (required && !value) { - return requiredError; + return required === true ? defaultRequiredError : required; } return undefined; diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx index 214f7386..70fd2e78 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/x/tests/useBooleanField.test.tsx @@ -88,7 +88,7 @@ describe('Boolean field', () => { }); it('Should set custom error if field is required and empty', async () => { - const [{ result }] = renderUseBooleanField({ required: true, requiredError: 'custom' }); + const [{ result }] = renderUseBooleanField({ required: 'custom' }); act(() => { result.current.control.setValue(null); From cb622d9c2d09662a96288d45ba24788e27b86d2a Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:14:20 +0300 Subject: [PATCH 5/5] Extracted BooleanFieldI18nContext --- packages/x/src/BooleanFieldI18n.tsx | 22 ++++++++++++++++++++++ packages/x/src/useBooleanField.ts | 11 +++++++---- packages/x/tests/useBooleanField.test.tsx | 23 ++++++++++++++++------- 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 packages/x/src/BooleanFieldI18n.tsx diff --git a/packages/x/src/BooleanFieldI18n.tsx b/packages/x/src/BooleanFieldI18n.tsx new file mode 100644 index 00000000..da548842 --- /dev/null +++ b/packages/x/src/BooleanFieldI18n.tsx @@ -0,0 +1,22 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export type BooleanFieldI18n = { + required: string; +}; + +export const defaultBooleanFieldI18n: BooleanFieldI18n = { + required: 'Field is required', +}; + +export const BooleanFieldI18nContext = createContext(defaultBooleanFieldI18n); + +export type BooleanFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const BooleanFieldI18nContextProvider = ({ i18n, children }: BooleanFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/x/src/useBooleanField.ts b/packages/x/src/useBooleanField.ts index 309c73b3..52575386 100644 --- a/packages/x/src/useBooleanField.ts +++ b/packages/x/src/useBooleanField.ts @@ -1,11 +1,12 @@ +import { useContext } from 'react'; import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; +import { BooleanFieldI18nContext } from './BooleanFieldI18n'; + export type BooleanFieldConfig = FieldConfig & { - required?: boolean | string; + required?: boolean; }; -export const defaultRequiredError = 'Field is required'; - export type BooleanFieldBag = FieldContext & { onBlur: () => void; }; @@ -17,6 +18,8 @@ export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => control: { setTouched }, } = fieldBag; + const i18n = useContext(BooleanFieldI18nContext); + const onBlur = () => { setTouched({ $touched: true }); }; @@ -25,7 +28,7 @@ export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => name: config.name, validator: (value) => { if (required && !value) { - return required === true ? defaultRequiredError : required; + return i18n.required; } return undefined; diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx index 70fd2e78..57dabc51 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/x/tests/useBooleanField.test.tsx @@ -2,14 +2,16 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { BooleanFieldConfig, defaultRequiredError, useBooleanField } from '../src/useBooleanField'; +import { BooleanFieldI18n, BooleanFieldI18nContextProvider, defaultBooleanFieldI18n } from '../src/BooleanFieldI18n'; +import { BooleanFieldConfig, useBooleanField } from '../src/useBooleanField'; type Config = Omit & { initialValue?: boolean; + i18n?: Partial; }; const renderUseBooleanField = (config: Config = {}) => { - const { initialValue = false, ...initialProps } = config; + const { initialValue = false, i18n, ...initialProps } = config; const formBag = renderHook(() => useForm({ @@ -27,7 +29,9 @@ const renderUseBooleanField = (config: Config = {}) => { }), { wrapper: ({ children }) => ( - {children} + + {children} + ), initialProps, }, @@ -59,7 +63,7 @@ describe('Boolean field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); }); act(() => { @@ -67,7 +71,7 @@ describe('Boolean field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); }); act(() => { @@ -75,7 +79,7 @@ describe('Boolean field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); }); act(() => { @@ -88,7 +92,12 @@ describe('Boolean field', () => { }); it('Should set custom error if field is required and empty', async () => { - const [{ result }] = renderUseBooleanField({ required: 'custom' }); + const [{ result }] = renderUseBooleanField({ + required: true, + i18n: { + required: 'custom', + }, + }); act(() => { result.current.control.setValue(null);