From 1b207812748eb77ecae674b525f4b9836f53c794 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sun, 27 Aug 2023 17:33:15 +0300 Subject: [PATCH 01/15] Created useStringField --- packages/x/src/useStringField.ts | 80 ++++++++++++++++++++++++ packages/x/tests/useStringField.test.tsx | 51 +++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/x/src/useStringField.ts create mode 100644 packages/x/tests/useStringField.test.tsx diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts new file mode 100644 index 00000000..92db52ae --- /dev/null +++ b/packages/x/src/useStringField.ts @@ -0,0 +1,80 @@ +import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; + +export type StringFieldErrorMessages = { + required: string; + shorterThanMinLength: ((minLength: number) => string) | string; + longerThanMaxLength: ((maxLength: number) => string) | string; +}; + +export const defaultErrorMessages: StringFieldErrorMessages = { + required: 'Field is required', + shorterThanMinLength: (minLength: number) => `String should not include less than ${minLength} character(s)`, + longerThanMaxLength: (maxLength: number) => `String should not include more than ${maxLength} character(s)`, +}; + +export type StringFieldConfig = FieldConfig & { + required?: boolean; + minLength?: number; + maxLength?: number; + + formatter?: (value: string) => string; + + errorMessages?: StringFieldErrorMessages; +}; + +export type StringFieldBag = FieldContext & { + onBlur: () => void; +}; + +export const useStringField = ({ + name, + validator, + schema, + required, + minLength, + maxLength, + formatter = (val) => val, + errorMessages = defaultErrorMessages, +}: StringFieldConfig) => { + const { required: requiredError, shorterThanMinLength, longerThanMaxLength } = errorMessages; + + const fieldBag = useField({ name, validator, schema }); + + const { + control: { setTouched, setValue }, + value, + } = fieldBag; + + useFieldValidator({ + name, + validator: (value: string | undefined | null) => { + const isValueEmpty = !value || value.trim().length === 0; + + if (required && isValueEmpty) { + return requiredError; + } + + if (typeof minLength === 'number' && ((isValueEmpty && minLength > 0) || value!.length < minLength)) { + return typeof shorterThanMinLength === 'function' + ? shorterThanMinLength(minLength) + : shorterThanMinLength; + } + + if (typeof maxLength === 'number' && value && value.length > maxLength) { + return typeof longerThanMaxLength === 'function' ? longerThanMaxLength(maxLength) : longerThanMaxLength; + } + + return undefined; + }, + }); + + const onBlur = () => { + setTouched({ $touched: true }); + setValue(formatter(value ?? '')); + }; + + return { + onBlur, + ...fieldBag, + }; +}; diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx new file mode 100644 index 00000000..d2819838 --- /dev/null +++ b/packages/x/tests/useStringField.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { StringFieldConfig, useStringField } from '../src/useStringField'; + +type Config = Omit & { + initialValue?: string; +}; + +const renderUseStringField = (config: Config = {}) => { + const { initialValue = '', ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const stringFieldBag = renderHook( + (props: Omit) => + useStringField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps, + }, + ); + + return [stringFieldBag, formBag] as const; +}; + +describe('String field', () => { + it('Should set touched=true on blur', async () => { + const [{ result }] = renderUseStringField(); + + await act(() => { + result.current.onBlur(); + }); + + await waitFor(() => { + expect(result.current.meta.touched?.$touched).toBeTruthy(); + }); + }); +}); From 04162263161377213625e5c114d2a043b387eb66 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 11:48:41 +0300 Subject: [PATCH 02/15] Added tests for useStringField --- packages/x/tests/useStringField.test.tsx | 90 +++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index d2819838..184fe20a 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { StringFieldConfig, useStringField } from '../src/useStringField'; +import { defaultErrorMessages, StringFieldConfig, useStringField } from '../src/useStringField'; type Config = Omit & { initialValue?: string; @@ -48,4 +48,92 @@ describe('String field', () => { expect(result.current.meta.touched?.$touched).toBeTruthy(); }); }); + + it('Should set default error if field is required and empty', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ required: true }); + + act(() => { + stringFieldBag.current.control.setValue(null); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + + act(() => { + stringFieldBag.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + + act(() => { + stringFieldBag.current.control.setValue(''); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + + act(() => { + stringFieldBag.current.control.setValue(' '); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + + act(() => { + stringFieldBag.current.control.setValue('a'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if value is longer than maxLength', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ maxLength: 3 }); + + act(() => { + stringFieldBag.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + stringFieldBag.current.control.setValue('aaaa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe( + (defaultErrorMessages.longerThanMaxLength as (maxLength: number) => string)(3), + ); + }); + }); + + it('Should set default error if value is shorter than minLength', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ minLength: 3 }); + + act(() => { + stringFieldBag.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + stringFieldBag.current.control.setValue('aa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe( + (defaultErrorMessages.shorterThanMinLength as (minLength: number) => string)(3), + ); + }); + }); }); From 92c70e97be255595fdda8b2ef35506d31387b3d4 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 11:50:21 +0300 Subject: [PATCH 03/15] Added changeset --- .changeset/chilly-clocks-train.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-clocks-train.md diff --git a/.changeset/chilly-clocks-train.md b/.changeset/chilly-clocks-train.md new file mode 100644 index 00000000..b6950e3f --- /dev/null +++ b/.changeset/chilly-clocks-train.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': patch +--- + +Created useStringField hook From 829eb496aa89ce196ddf5ce336cff4d405f72b3f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 12:09:02 +0300 Subject: [PATCH 04/15] Added tests for custom errors --- packages/x/src/useStringField.ts | 16 ++-- packages/x/tests/useStringField.test.tsx | 97 ++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index 92db52ae..ed224e74 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -2,8 +2,8 @@ import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactiv export type StringFieldErrorMessages = { required: string; - shorterThanMinLength: ((minLength: number) => string) | string; - longerThanMaxLength: ((maxLength: number) => string) | string; + shorterThanMinLength: (minLength: number) => string; + longerThanMaxLength: (maxLength: number) => string; }; export const defaultErrorMessages: StringFieldErrorMessages = { @@ -19,7 +19,7 @@ export type StringFieldConfig = FieldConfig & { formatter?: (value: string) => string; - errorMessages?: StringFieldErrorMessages; + errorMessages?: Partial; }; export type StringFieldBag = FieldContext & { @@ -36,8 +36,6 @@ export const useStringField = ({ formatter = (val) => val, errorMessages = defaultErrorMessages, }: StringFieldConfig) => { - const { required: requiredError, shorterThanMinLength, longerThanMaxLength } = errorMessages; - const fieldBag = useField({ name, validator, schema }); const { @@ -51,17 +49,15 @@ export const useStringField = ({ const isValueEmpty = !value || value.trim().length === 0; if (required && isValueEmpty) { - return requiredError; + return errorMessages.required ?? defaultErrorMessages.required; } if (typeof minLength === 'number' && ((isValueEmpty && minLength > 0) || value!.length < minLength)) { - return typeof shorterThanMinLength === 'function' - ? shorterThanMinLength(minLength) - : shorterThanMinLength; + return (errorMessages.shorterThanMinLength ?? defaultErrorMessages.shorterThanMinLength)(minLength); } if (typeof maxLength === 'number' && value && value.length > maxLength) { - return typeof longerThanMaxLength === 'function' ? longerThanMaxLength(maxLength) : longerThanMaxLength; + return (errorMessages.longerThanMaxLength ?? defaultErrorMessages.longerThanMaxLength)(maxLength); } return undefined; diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 184fe20a..45c581e7 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -136,4 +136,101 @@ describe('String field', () => { ); }); }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ + required: true, + errorMessages: { + required: 'custom', + }, + }); + + act(() => { + stringFieldBag.current.control.setValue(null); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + stringFieldBag.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + stringFieldBag.current.control.setValue(''); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + stringFieldBag.current.control.setValue(' '); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + stringFieldBag.current.control.setValue('a'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if value is longer than maxLength', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ + maxLength: 3, + errorMessages: { longerThanMaxLength: () => 'custom' }, + }); + + act(() => { + stringFieldBag.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + stringFieldBag.current.control.setValue('aaaa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if value is shorter than minLength', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ + minLength: 3, + errorMessages: { + shorterThanMinLength: () => 'custom', + }, + }); + + act(() => { + stringFieldBag.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + stringFieldBag.current.control.setValue('aa'); + }); + + await waitFor(() => { + expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + }); + }); }); From f038b5aecf8d885de97c65621f68453921ab4ee4 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 12:12:02 +0300 Subject: [PATCH 05/15] Added test for `formatter` prop --- packages/x/tests/useStringField.test.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 45c581e7..f5363918 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -233,4 +233,19 @@ describe('String field', () => { expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); }); }); + + it('Should set formatted value in form state on blur', async () => { + const [{ result: stringFieldBag }] = renderUseStringField({ + formatter: (value) => `+${value}`, + initialValue: 'hello', + }); + + await act(() => { + stringFieldBag.current.onBlur(); + }); + + await waitFor(() => { + expect(stringFieldBag.current.value).toBe('+hello'); + }); + }); }); From e099312082c789dbda273319926669d8edcb26a7 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:57:54 +0300 Subject: [PATCH 06/15] Refactoring --- packages/x/tests/useStringField.test.tsx | 94 ++++++++++++------------ 1 file changed, 45 insertions(+), 49 deletions(-) diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index f5363918..7e81b533 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -50,95 +50,91 @@ describe('String field', () => { }); it('Should set default error if field is required and empty', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ required: true }); + const [{ result }] = renderUseStringField({ required: true }); act(() => { - stringFieldBag.current.control.setValue(null); + result.current.control.setValue(null); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); }); act(() => { - stringFieldBag.current.control.setValue(undefined); + result.current.control.setValue(undefined); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); }); act(() => { - stringFieldBag.current.control.setValue(''); + result.current.control.setValue(''); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); }); act(() => { - stringFieldBag.current.control.setValue(' '); + result.current.control.setValue(' '); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); }); act(() => { - stringFieldBag.current.control.setValue('a'); + result.current.control.setValue('a'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); it('Should set default error if value is longer than maxLength', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ maxLength: 3 }); + const [{ result }] = renderUseStringField({ maxLength: 3 }); act(() => { - stringFieldBag.current.control.setValue('aaa'); + result.current.control.setValue('aaa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); act(() => { - stringFieldBag.current.control.setValue('aaaa'); + result.current.control.setValue('aaaa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe( - (defaultErrorMessages.longerThanMaxLength as (maxLength: number) => string)(3), - ); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.longerThanMaxLength(3)); }); }); it('Should set default error if value is shorter than minLength', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ minLength: 3 }); + const [{ result }] = renderUseStringField({ minLength: 3 }); act(() => { - stringFieldBag.current.control.setValue('aaa'); + result.current.control.setValue('aaa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); act(() => { - stringFieldBag.current.control.setValue('aa'); + result.current.control.setValue('aa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe( - (defaultErrorMessages.shorterThanMinLength as (minLength: number) => string)(3), - ); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.shorterThanMinLength(3)); }); }); it('Should set custom error if field is required and empty', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ + const [{ result }] = renderUseStringField({ required: true, errorMessages: { required: 'custom', @@ -146,71 +142,71 @@ describe('String field', () => { }); act(() => { - stringFieldBag.current.control.setValue(null); + result.current.control.setValue(null); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); act(() => { - stringFieldBag.current.control.setValue(undefined); + result.current.control.setValue(undefined); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); act(() => { - stringFieldBag.current.control.setValue(''); + result.current.control.setValue(''); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); act(() => { - stringFieldBag.current.control.setValue(' '); + result.current.control.setValue(' '); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); act(() => { - stringFieldBag.current.control.setValue('a'); + result.current.control.setValue('a'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); it('Should set custom error if value is longer than maxLength', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ + const [{ result }] = renderUseStringField({ maxLength: 3, errorMessages: { longerThanMaxLength: () => 'custom' }, }); act(() => { - stringFieldBag.current.control.setValue('aaa'); + result.current.control.setValue('aaa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); act(() => { - stringFieldBag.current.control.setValue('aaaa'); + result.current.control.setValue('aaaa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); }); it('Should set custom error if value is shorter than minLength', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ + const [{ result }] = renderUseStringField({ minLength: 3, errorMessages: { shorterThanMinLength: () => 'custom', @@ -218,34 +214,34 @@ describe('String field', () => { }); act(() => { - stringFieldBag.current.control.setValue('aaa'); + result.current.control.setValue('aaa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); act(() => { - stringFieldBag.current.control.setValue('aa'); + result.current.control.setValue('aa'); }); await waitFor(() => { - expect(stringFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); }); it('Should set formatted value in form state on blur', async () => { - const [{ result: stringFieldBag }] = renderUseStringField({ + const [{ result }] = renderUseStringField({ formatter: (value) => `+${value}`, initialValue: 'hello', }); await act(() => { - stringFieldBag.current.onBlur(); + result.current.onBlur(); }); await waitFor(() => { - expect(stringFieldBag.current.value).toBe('+hello'); + expect(result.current.value).toBe('+hello'); }); }); }); From 428c51f869fde6aef72c6721cad49505cecaf939 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 11:35:56 +0300 Subject: [PATCH 07/15] Refactored API for custom errors --- packages/x/src/useStringField.ts | 54 +++++++++++++----------- packages/x/tests/useStringField.test.tsx | 33 +++++++-------- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index ed224e74..55a33268 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -1,25 +1,20 @@ import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; +import { isFunction } from 'lodash'; -export type StringFieldErrorMessages = { - required: string; - shorterThanMinLength: (minLength: number) => string; - longerThanMaxLength: (maxLength: number) => string; -}; +export const defaultRequiredError = 'Field is required'; +export const defaultMinLengthError = (minLength: number) => + `String should not include less than ${minLength} character(s)`; +export const defaultMaxLengthError = (maxLength: number) => + `String should not include more than ${maxLength} character(s)`; -export const defaultErrorMessages: StringFieldErrorMessages = { - required: 'Field is required', - shorterThanMinLength: (minLength: number) => `String should not include less than ${minLength} character(s)`, - longerThanMaxLength: (maxLength: number) => `String should not include more than ${maxLength} character(s)`, -}; +export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; export type StringFieldConfig = FieldConfig & { - required?: boolean; - minLength?: number; - maxLength?: number; - formatter?: (value: string) => string; - errorMessages?: Partial; + required?: boolean | string; + minLength?: number | ErrorTuple; + maxLength?: number | ErrorTuple; }; export type StringFieldBag = FieldContext & { @@ -30,12 +25,11 @@ export const useStringField = ({ name, validator, schema, - required, - minLength, - maxLength, formatter = (val) => val, - errorMessages = defaultErrorMessages, + ...validationOptions }: StringFieldConfig) => { + const { required, minLength, maxLength } = validationOptions; + const fieldBag = useField({ name, validator, schema }); const { @@ -49,15 +43,27 @@ export const useStringField = ({ const isValueEmpty = !value || value.trim().length === 0; if (required && isValueEmpty) { - return errorMessages.required ?? defaultErrorMessages.required; + return required === true ? defaultRequiredError : required; } - if (typeof minLength === 'number' && ((isValueEmpty && minLength > 0) || value!.length < minLength)) { - return (errorMessages.shorterThanMinLength ?? defaultErrorMessages.shorterThanMinLength)(minLength); + if (minLength) { + if (Array.isArray(minLength)) { + const [length, message] = minLength; + + return isFunction(message) ? message(length) : message; + } + + return defaultMinLengthError(minLength); } - if (typeof maxLength === 'number' && value && value.length > maxLength) { - return (errorMessages.longerThanMaxLength ?? defaultErrorMessages.longerThanMaxLength)(maxLength); + if (maxLength) { + if (Array.isArray(maxLength)) { + const [length, message] = maxLength; + + return isFunction(message) ? message(length) : message; + } + + return defaultMaxLengthError(maxLength); } return undefined; diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 7e81b533..6feab1cc 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -2,7 +2,13 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { defaultErrorMessages, StringFieldConfig, useStringField } from '../src/useStringField'; +import { + defaultMaxLengthError, + defaultMinLengthError, + defaultRequiredError, + StringFieldConfig, + useStringField, +} from '../src/useStringField'; type Config = Omit & { initialValue?: string; @@ -57,7 +63,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); }); act(() => { @@ -65,7 +71,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); }); act(() => { @@ -73,7 +79,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); }); act(() => { @@ -81,7 +87,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); }); act(() => { @@ -109,7 +115,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.longerThanMaxLength(3)); + expect(result.current.meta.error?.$error).toBe(defaultMaxLengthError(3)); }); }); @@ -129,16 +135,13 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.shorterThanMinLength(3)); + expect(result.current.meta.error?.$error).toBe(defaultMinLengthError(3)); }); }); it('Should set custom error if field is required and empty', async () => { const [{ result }] = renderUseStringField({ - required: true, - errorMessages: { - required: 'custom', - }, + required: 'custom', }); act(() => { @@ -184,8 +187,7 @@ describe('String field', () => { it('Should set custom error if value is longer than maxLength', async () => { const [{ result }] = renderUseStringField({ - maxLength: 3, - errorMessages: { longerThanMaxLength: () => 'custom' }, + maxLength: [3, 'custom'], }); act(() => { @@ -207,10 +209,7 @@ describe('String field', () => { it('Should set custom error if value is shorter than minLength', async () => { const [{ result }] = renderUseStringField({ - minLength: 3, - errorMessages: { - shorterThanMinLength: () => 'custom', - }, + minLength: [3, 'custom'], }); act(() => { From 76546bdd504f06746711d0015182cdcce60aa244 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 11:41:53 +0300 Subject: [PATCH 08/15] Fixed lodash import --- packages/x/src/useStringField.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index 55a33268..cf9d2bf4 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -1,5 +1,5 @@ import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; -import { isFunction } from 'lodash'; +import isFunction from 'lodash/isFunction'; export const defaultRequiredError = 'Field is required'; export const defaultMinLengthError = (minLength: number) => From 95e2d5351273262c22d9a766052a94eab4df8eb4 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:05:59 +0300 Subject: [PATCH 09/15] Fixed StringField validator --- packages/x/src/useStringField.ts | 18 ++++++++++++------ packages/x/tests/useStringField.test.tsx | 16 ++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index cf9d2bf4..c8f6867c 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -46,24 +46,30 @@ export const useStringField = ({ return required === true ? defaultRequiredError : required; } + const valueLength = value?.length ?? 0; + if (minLength) { if (Array.isArray(minLength)) { const [length, message] = minLength; - return isFunction(message) ? message(length) : message; + if (valueLength < length) { + return isFunction(message) ? message(length) : message; + } + } else if (valueLength < minLength) { + return defaultMinLengthError(minLength); } - - return defaultMinLengthError(minLength); } if (maxLength) { if (Array.isArray(maxLength)) { const [length, message] = maxLength; - return isFunction(message) ? message(length) : message; + if (valueLength > length) { + return isFunction(message) ? message(length) : message; + } + } else if (valueLength > maxLength) { + return defaultMaxLengthError(maxLength); } - - return defaultMaxLengthError(maxLength); } return undefined; diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 6feab1cc..f16a75ad 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -103,19 +103,19 @@ describe('String field', () => { const [{ result }] = renderUseStringField({ maxLength: 3 }); act(() => { - result.current.control.setValue('aaa'); + result.current.control.setValue('aaaa'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBe(defaultMaxLengthError(3)); }); act(() => { - result.current.control.setValue('aaaa'); + result.current.control.setValue('aaa'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMaxLengthError(3)); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); @@ -123,19 +123,19 @@ describe('String field', () => { const [{ result }] = renderUseStringField({ minLength: 3 }); act(() => { - result.current.control.setValue('aaa'); + result.current.control.setValue('aa'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBe(defaultMinLengthError(3)); }); act(() => { - result.current.control.setValue('aa'); + result.current.control.setValue('aaa'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMinLengthError(3)); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); From b6ac825d564bd77c44c270dc3785d26c7d18b175 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:46:46 +0300 Subject: [PATCH 10/15] Covered more test cases in useStringField --- packages/x/tests/useStringField.test.tsx | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index f16a75ad..37f90a89 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -207,6 +207,30 @@ describe('String field', () => { }); }); + it('Should set custom error if value is longer than maxLength (with callback)', async () => { + const callback = jest.fn(() => 'custom'); + const [{ result }] = renderUseStringField({ + maxLength: [3, callback], + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + result.current.control.setValue('aaaa'); + }); + + await waitFor(() => { + expect(callback).toBeCalledWith(3); + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + it('Should set custom error if value is shorter than minLength', async () => { const [{ result }] = renderUseStringField({ minLength: [3, 'custom'], @@ -229,6 +253,30 @@ describe('String field', () => { }); }); + it('Should set custom error if value is shorter than minLength', async () => { + const callback = jest.fn(() => 'custom'); + const [{ result }] = renderUseStringField({ + minLength: [3, callback], + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + result.current.control.setValue('aa'); + }); + + await waitFor(() => { + expect(callback).toBeCalledWith(3); + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + it('Should set formatted value in form state on blur', async () => { const [{ result }] = renderUseStringField({ formatter: (value) => `+${value}`, From cf8ef56b976145820c6ee5d293669a374ae37753 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:49:47 +0300 Subject: [PATCH 11/15] Refactoring --- packages/x/src/useStringField.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index c8f6867c..515003c1 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -26,10 +26,10 @@ export const useStringField = ({ validator, schema, formatter = (val) => val, - ...validationOptions + required, + maxLength, + minLength, }: StringFieldConfig) => { - const { required, minLength, maxLength } = validationOptions; - const fieldBag = useField({ name, validator, schema }); const { From f6c637a498930b93b84ac35be5f6faa60103c19c Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sun, 10 Sep 2023 16:45:25 +0300 Subject: [PATCH 12/15] Refactoring --- packages/x/src/useStringField.ts | 16 ++++++++-------- packages/x/tests/useStringField.test.tsx | 20 +++++++------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index 515003c1..31aabe0c 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -1,11 +1,11 @@ import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; import isFunction from 'lodash/isFunction'; -export const defaultRequiredError = 'Field is required'; -export const defaultMinLengthError = (minLength: number) => - `String should not include less than ${minLength} character(s)`; -export const defaultMaxLengthError = (maxLength: number) => - `String should not include more than ${maxLength} character(s)`; +export const defaultErrors = { + required: 'Field is required', + minLength: (minLength: number) => `String should not include less than ${minLength} character(s)`, + maxLength: (maxLength: number) => `String should not include more than ${maxLength} character(s)`, +}; export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; @@ -43,7 +43,7 @@ export const useStringField = ({ const isValueEmpty = !value || value.trim().length === 0; if (required && isValueEmpty) { - return required === true ? defaultRequiredError : required; + return required === true ? defaultErrors.required : required; } const valueLength = value?.length ?? 0; @@ -56,7 +56,7 @@ export const useStringField = ({ return isFunction(message) ? message(length) : message; } } else if (valueLength < minLength) { - return defaultMinLengthError(minLength); + return defaultErrors.minLength(minLength); } } @@ -68,7 +68,7 @@ export const useStringField = ({ return isFunction(message) ? message(length) : message; } } else if (valueLength > maxLength) { - return defaultMaxLengthError(maxLength); + return defaultErrors.maxLength(maxLength); } } diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 37f90a89..205bfa0a 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -2,13 +2,7 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { - defaultMaxLengthError, - defaultMinLengthError, - defaultRequiredError, - StringFieldConfig, - useStringField, -} from '../src/useStringField'; +import { defaultErrors, StringFieldConfig, useStringField } from '../src/useStringField'; type Config = Omit & { initialValue?: string; @@ -63,7 +57,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultErrors.required); }); act(() => { @@ -71,7 +65,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultErrors.required); }); act(() => { @@ -79,7 +73,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultErrors.required); }); act(() => { @@ -87,7 +81,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultErrors.required); }); act(() => { @@ -107,7 +101,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMaxLengthError(3)); + expect(result.current.meta.error?.$error).toBe(defaultErrors.maxLength(3)); }); act(() => { @@ -127,7 +121,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMinLengthError(3)); + expect(result.current.meta.error?.$error).toBe(defaultErrors.minLength(3)); }); act(() => { From a6e9d71a092d2330d437acd4127a6dc86f28b03f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sun, 10 Sep 2023 16:46:46 +0300 Subject: [PATCH 13/15] Removed `formatter` prop --- packages/x/src/useStringField.ts | 21 +++++---------------- packages/x/tests/useStringField.test.tsx | 15 --------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index 31aabe0c..c0c1da65 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; import isFunction from 'lodash/isFunction'; @@ -10,8 +11,6 @@ export const defaultErrors = { export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; export type StringFieldConfig = FieldConfig & { - formatter?: (value: string) => string; - required?: boolean | string; minLength?: number | ErrorTuple; maxLength?: number | ErrorTuple; @@ -21,20 +20,11 @@ export type StringFieldBag = FieldContext & { onBlur: () => void; }; -export const useStringField = ({ - name, - validator, - schema, - formatter = (val) => val, - required, - maxLength, - minLength, -}: StringFieldConfig) => { +export const useStringField = ({ name, validator, schema, required, maxLength, minLength }: StringFieldConfig) => { const fieldBag = useField({ name, validator, schema }); const { - control: { setTouched, setValue }, - value, + control: { setTouched }, } = fieldBag; useFieldValidator({ @@ -76,10 +66,9 @@ export const useStringField = ({ }, }); - const onBlur = () => { + const onBlur = useCallback(() => { setTouched({ $touched: true }); - setValue(formatter(value ?? '')); - }; + }, [setTouched]); return { onBlur, diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 205bfa0a..5adbbdb9 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.test.tsx @@ -270,19 +270,4 @@ describe('String field', () => { expect(result.current.meta.error?.$error).toBe('custom'); }); }); - - it('Should set formatted value in form state on blur', async () => { - const [{ result }] = renderUseStringField({ - formatter: (value) => `+${value}`, - initialValue: 'hello', - }); - - await act(() => { - result.current.onBlur(); - }); - - await waitFor(() => { - expect(result.current.value).toBe('+hello'); - }); - }); }); From 44c3fcce34d04840516674d0220278fb21e1c847 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 15:48:49 +0300 Subject: [PATCH 14/15] Extracted StringFieldI18nContext --- packages/x/src/StringFieldI18n.tsx | 26 +++++++++++++ packages/x/src/useStringField.ts | 45 ++++++----------------- packages/x/tests/useStringField.test.tsx | 47 +++++++++++++++++------- 3 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 packages/x/src/StringFieldI18n.tsx diff --git a/packages/x/src/StringFieldI18n.tsx b/packages/x/src/StringFieldI18n.tsx new file mode 100644 index 00000000..453d0e05 --- /dev/null +++ b/packages/x/src/StringFieldI18n.tsx @@ -0,0 +1,26 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import { merge } from 'lodash'; + +export type StringFieldI18n = { + required: string; + minLength: (length: number) => string; + maxLength: (length: number) => string; +}; + +export const defaultStringFieldI18n: StringFieldI18n = { + required: 'Field is required', + minLength: (minLength: number) => `String should not include less than ${minLength} character(s)`, + maxLength: (maxLength: number) => `String should not include more than ${maxLength} character(s)`, +}; + +export const StringFieldI18nContext = createContext(defaultStringFieldI18n); + +export type StringFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const StringFieldI18nContextProvider = ({ i18n, children }: StringFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/x/src/useStringField.ts b/packages/x/src/useStringField.ts index c0c1da65..fdf49520 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/x/src/useStringField.ts @@ -1,19 +1,12 @@ -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; -import isFunction from 'lodash/isFunction'; -export const defaultErrors = { - required: 'Field is required', - minLength: (minLength: number) => `String should not include less than ${minLength} character(s)`, - maxLength: (maxLength: number) => `String should not include more than ${maxLength} character(s)`, -}; - -export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; +import { StringFieldI18nContext } from './StringFieldI18n'; export type StringFieldConfig = FieldConfig & { - required?: boolean | string; - minLength?: number | ErrorTuple; - maxLength?: number | ErrorTuple; + required?: boolean; + minLength?: number; + maxLength?: number; }; export type StringFieldBag = FieldContext & { @@ -27,39 +20,25 @@ export const useStringField = ({ name, validator, schema, required, maxLength, m control: { setTouched }, } = fieldBag; + const i18n = useContext(StringFieldI18nContext); + useFieldValidator({ name, validator: (value: string | undefined | null) => { const isValueEmpty = !value || value.trim().length === 0; if (required && isValueEmpty) { - return required === true ? defaultErrors.required : required; + return i18n.required; } const valueLength = value?.length ?? 0; - if (minLength) { - if (Array.isArray(minLength)) { - const [length, message] = minLength; - - if (valueLength < length) { - return isFunction(message) ? message(length) : message; - } - } else if (valueLength < minLength) { - return defaultErrors.minLength(minLength); - } + if (typeof minLength === 'number' && valueLength < minLength) { + return i18n.minLength(minLength); } - if (maxLength) { - if (Array.isArray(maxLength)) { - const [length, message] = maxLength; - - if (valueLength > length) { - return isFunction(message) ? message(length) : message; - } - } else if (valueLength > maxLength) { - return defaultErrors.maxLength(maxLength); - } + if (typeof maxLength === 'number' && valueLength > maxLength) { + return i18n.maxLength(maxLength); } return undefined; diff --git a/packages/x/tests/useStringField.test.tsx b/packages/x/tests/useStringField.test.tsx index 5adbbdb9..bf296ea9 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/x/tests/useStringField.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 { defaultErrors, StringFieldConfig, useStringField } from '../src/useStringField'; +import { defaultStringFieldI18n, StringFieldI18n, StringFieldI18nContextProvider } from '../src/StringFieldI18n'; +import { StringFieldConfig, useStringField } from '../src/useStringField'; type Config = Omit & { initialValue?: string; + i18n?: Partial; }; const renderUseStringField = (config: Config = {}) => { - const { initialValue = '', ...initialProps } = config; + const { initialValue = '', i18n, ...initialProps } = config; const formBag = renderHook(() => useForm({ @@ -27,7 +29,9 @@ const renderUseStringField = (config: Config = {}) => { }), { wrapper: ({ children }) => ( - {children} + + {children} + ), initialProps, }, @@ -57,7 +61,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrors.required); + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); }); act(() => { @@ -65,7 +69,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrors.required); + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); }); act(() => { @@ -73,7 +77,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrors.required); + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); }); act(() => { @@ -81,7 +85,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrors.required); + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); }); act(() => { @@ -101,7 +105,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrors.maxLength(3)); + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.maxLength(3)); }); act(() => { @@ -121,7 +125,7 @@ describe('String field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrors.minLength(3)); + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.minLength(3)); }); act(() => { @@ -135,7 +139,10 @@ describe('String field', () => { it('Should set custom error if field is required and empty', async () => { const [{ result }] = renderUseStringField({ - required: 'custom', + required: true, + i18n: { + required: 'custom', + }, }); act(() => { @@ -181,7 +188,10 @@ describe('String field', () => { it('Should set custom error if value is longer than maxLength', async () => { const [{ result }] = renderUseStringField({ - maxLength: [3, 'custom'], + maxLength: 3, + i18n: { + maxLength: () => 'custom', + }, }); act(() => { @@ -204,7 +214,10 @@ describe('String field', () => { it('Should set custom error if value is longer than maxLength (with callback)', async () => { const callback = jest.fn(() => 'custom'); const [{ result }] = renderUseStringField({ - maxLength: [3, callback], + maxLength: 3, + i18n: { + maxLength: callback, + }, }); act(() => { @@ -227,7 +240,10 @@ describe('String field', () => { it('Should set custom error if value is shorter than minLength', async () => { const [{ result }] = renderUseStringField({ - minLength: [3, 'custom'], + minLength: 3, + i18n: { + minLength: () => 'custom', + }, }); act(() => { @@ -250,7 +266,10 @@ describe('String field', () => { it('Should set custom error if value is shorter than minLength', async () => { const callback = jest.fn(() => 'custom'); const [{ result }] = renderUseStringField({ - minLength: [3, callback], + minLength: 3, + i18n: { + minLength: callback, + }, }); act(() => { From 581cafe0a82d7db0eea49cff87abe4a059c598c9 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:05:48 +0300 Subject: [PATCH 15/15] Fixed lodash import --- packages/x/src/StringFieldI18n.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x/src/StringFieldI18n.tsx b/packages/x/src/StringFieldI18n.tsx index 453d0e05..a62f3f46 100644 --- a/packages/x/src/StringFieldI18n.tsx +++ b/packages/x/src/StringFieldI18n.tsx @@ -1,5 +1,5 @@ import React, { createContext, PropsWithChildren } from 'react'; -import { merge } from 'lodash'; +import merge from 'lodash/merge'; export type StringFieldI18n = { required: string;