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(() => {