diff --git a/packages/x/src/DecimalFieldI18n.tsx b/packages/x/src/DecimalFieldI18n.tsx new file mode 100644 index 00000000..f08f59d1 --- /dev/null +++ b/packages/x/src/DecimalFieldI18n.tsx @@ -0,0 +1,36 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export const defaultFormat = (value: number | null | undefined, precision: number) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toFixed(precision).toString(); +}; + +export type DecimalFieldI18n = { + required: string; + invalidInput: string; + minValue: (value: number, precision: number) => string; + maxValue: (value: number, precision: number) => string; +}; + +export const defaultDecimalFieldI18n: DecimalFieldI18n = { + required: 'Field is required', + invalidInput: 'Must be decimal', + minValue: (min: number, precision: number) => `Value should not be less than ${defaultFormat(min, precision)}`, + maxValue: (max: number, precision: number) => `Value should not be greater than ${defaultFormat(max, precision)}`, +}; + +export const DecimalFieldI18nContext = createContext(defaultDecimalFieldI18n); + +export type DecimalFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const DecimalFieldI18nContextProvider = ({ i18n, children }: DecimalFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index 7d6f725b..7ac9a252 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -1,34 +1,16 @@ -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; -import { isFunction, isNil } from 'lodash'; +import { DecimalFieldI18nContext, defaultFormat } from './DecimalFieldI18n'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; const DECIMAL_REGEX = /^\d*\.?\d*$/; export const defaultPrecision = 2; -export const defaultFormat = (value: number | null | undefined, precision: number) => { - if (typeof value !== 'number' || !Number.isFinite(value)) { - return ''; - } - - return value.toFixed(precision).toString(); -}; - -export const defaultRequiredError = 'Field is required'; -export const defaultInvalidInputError = 'Must be decimal'; -export const defaultMinValueError = (min: number, precision: number) => - `Value should not be less than ${defaultFormat(min, precision)}`; -export const defaultMaxValueError = (max: number, precision: number) => - `Value should not be more than ${defaultFormat(max, precision)}`; - -export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; - export type DecimalFieldConfig = FieldConfig & { - required?: boolean | string; - invalidInput?: string; - min?: number | ErrorTuple; - max?: number | ErrorTuple; + required?: boolean; + min?: number; + max?: number; format?: (value: number | null | undefined, precision: number) => string; parse?: (text: string) => number; @@ -43,13 +25,14 @@ export const useDecimalField = ({ validator, schema, required, - invalidInput, min, max, format, parse, precision = defaultPrecision, }: DecimalFieldConfig): DecimalFieldBag => { + const i18n = useContext(DecimalFieldI18nContext); + const defaultParse = useCallback( (text: string) => { text = text.trim(); @@ -58,26 +41,19 @@ export const useDecimalField = ({ return null; } - const parseError = invalidInput ?? defaultInvalidInputError; - if (!DECIMAL_REGEX.test(text)) { - throw new ConversionError(parseError); + throw new ConversionError(i18n.invalidInput); } const value = Number.parseFloat(text); if (Number.isNaN(value)) { - // "." is valid decimal number zero, however Number.parseFloat returns NaN - if (text === '.') { - return 0; - } - - throw new ConversionError(parseError); + throw new ConversionError(i18n.invalidInput); } return value; }, - [invalidInput], + [i18n.invalidInput], ); const formatValue = useCallback( @@ -99,35 +75,19 @@ export const useDecimalField = ({ name, validator: (value) => { if (required && typeof value !== 'number') { - return required === true ? defaultRequiredError : required; + return i18n.required; } if (typeof value !== 'number') { return undefined; } - if (!isNil(min)) { - if (Array.isArray(min)) { - const [minValue, message] = min; - - if (value < minValue) { - return isFunction(message) ? message(value) : message; - } - } else if (value < min) { - return defaultMinValueError(min, precision); - } + if (typeof min === 'number' && value < min) { + return i18n.minValue(min, precision); } - if (!isNil(max)) { - if (Array.isArray(max)) { - const [maxValue, message] = max; - - if (value > maxValue) { - return isFunction(message) ? message(value) : message; - } - } else if (value > max) { - return defaultMaxValueError(max, precision); - } + if (typeof max === 'number' && value > max) { + return i18n.maxValue(max, precision); } return undefined; diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx index 817acc7b..df288b19 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/x/tests/useDecimalField.test.tsx @@ -3,22 +3,20 @@ import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; import { - DecimalFieldConfig, + DecimalFieldI18n, + DecimalFieldI18nContextProvider, + defaultDecimalFieldI18n, defaultFormat, - defaultInvalidInputError, - defaultMaxValueError, - defaultMinValueError, - defaultPrecision, - defaultRequiredError, - useDecimalField, -} from '../src/useDecimalField'; +} from '../src/DecimalFieldI18n'; +import { DecimalFieldConfig, defaultPrecision, useDecimalField } from '../src/useDecimalField'; type Config = Omit & { initialValue?: number | null; + i18n?: Partial; }; const renderUseDecimalField = (config: Config = {}) => { - const { initialValue = 0, ...initialProps } = config; + const { initialValue = 0, i18n, ...initialProps } = config; const formBag = renderHook(() => useForm({ @@ -36,7 +34,9 @@ const renderUseDecimalField = (config: Config = {}) => { }), { wrapper: ({ children }) => ( - {children} + + {children} + ), initialProps, }, @@ -61,7 +61,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); }); await act(() => { @@ -69,7 +69,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); }); await act(() => { @@ -77,7 +77,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); }); await act(() => { @@ -113,8 +113,8 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBeUndefined(); + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); }); await act(() => { @@ -153,7 +153,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.required); }); }); @@ -165,7 +165,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMinValueError(0.5, defaultPrecision)); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.minValue(0.5, defaultPrecision)); }); act(() => { @@ -185,7 +185,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMaxValueError(0.5, defaultPrecision)); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.maxValue(0.5, defaultPrecision)); }); act(() => { @@ -199,7 +199,9 @@ describe('Decimal field', () => { it('Should set custom conversion error correctly', async () => { const [{ result }] = renderUseDecimalField({ - invalidInput: 'custom', + i18n: { + invalidInput: 'custom', + }, }); await act(() => { @@ -229,7 +231,10 @@ describe('Decimal field', () => { it('Should set custom error if field is required and empty', async () => { const [{ result }] = renderUseDecimalField({ - required: 'custom', + required: true, + i18n: { + required: 'custom', + }, }); act(() => { @@ -243,7 +248,10 @@ describe('Decimal field', () => { it('Should set custom error if field value is less than min', async () => { const [{ result }] = renderUseDecimalField({ - min: [0.5, 'custom'], + min: 0.5, + i18n: { + minValue: () => 'custom', + }, }); act(() => { @@ -257,7 +265,10 @@ describe('Decimal field', () => { it('Should set custom error if field value is more than max', async () => { const [{ result }] = renderUseDecimalField({ - max: [0.5, 'custom'], + max: 0.5, + i18n: { + maxValue: () => 'custom', + }, }); act(() => {