Skip to content

Commit

Permalink
Refactored DecimalField custom errors API
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexShukel committed Aug 30, 2023
1 parent 191422f commit 581c7cc
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 46 deletions.
64 changes: 38 additions & 26 deletions packages/x/src/useDecimalField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { FieldConfig, useFieldValidator } from '@reactive-forms/core';
import { isFunction, isNil } from 'lodash';

import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField';

Expand All @@ -14,28 +15,23 @@ export const defaultFormat = (value: number | null | undefined, precision: numbe
return value.toFixed(precision).toString();
};

export type DecimalFieldErrorMessages = {
invalidInput: string;
required: string;
lessThanMinValue: (min: number, precision: number) => string;
moreThanMaxValue: (max: number, precision: number) => string;
};
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 const defaultErrorMessages: DecimalFieldErrorMessages = {
invalidInput: 'Must be decimal',
required: 'Field is required',
lessThanMinValue: (min, precision) => `Value should not be less than ${defaultFormat(min, precision)}`,
moreThanMaxValue: (max, precision) => `Value should not be more than ${defaultFormat(max, precision)}`,
};
export type ErrorTuple<T> = [value: T, message: string | ((value: T) => string)];

export type DecimalFieldConfig = FieldConfig<number | null | undefined> & {
required?: boolean;
min?: number;
max?: number;
required?: boolean | string;
invalidInput?: string;
min?: number | ErrorTuple<number>;
max?: number | ErrorTuple<number>;

format?: (value: number | null | undefined, precision: number) => string;
parse?: (text: string) => number;
errorMessages?: Partial<DecimalFieldErrorMessages>;

precision?: number;
};
Expand All @@ -47,11 +43,11 @@ export const useDecimalField = ({
validator,
schema,
required,
invalidInput,
min,
max,
format,
parse,
errorMessages = defaultErrorMessages,
precision = defaultPrecision,
}: DecimalFieldConfig): DecimalFieldBag => {
const defaultParse = useCallback(
Expand All @@ -62,10 +58,10 @@ export const useDecimalField = ({
return null;
}

const errorMessage = errorMessages.invalidInput ?? defaultErrorMessages.invalidInput;
const parseError = invalidInput ?? defaultInvalidInputError;

if (!DECIMAL_REGEX.test(text)) {
throw new ConversionError(errorMessage);
throw new ConversionError(parseError);
}

const value = Number.parseFloat(text);
Expand All @@ -76,12 +72,12 @@ export const useDecimalField = ({
return 0;
}

throw new ConversionError(errorMessage);
throw new ConversionError(parseError);
}

return value;
},
[errorMessages.invalidInput],
[invalidInput],
);

const formatValue = useCallback(
Expand All @@ -103,19 +99,35 @@ export const useDecimalField = ({
name,
validator: (value) => {
if (required && typeof value !== 'number') {
return errorMessages.required ?? defaultErrorMessages.required;
return required === true ? defaultRequiredError : required;
}

if (typeof value !== 'number') {
return undefined;
}

if (typeof min === 'number' && value < min) {
return (errorMessages.lessThanMinValue ?? defaultErrorMessages.lessThanMinValue)(min, precision);
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 max === 'number' && value > max) {
return (errorMessages.moreThanMaxValue ?? defaultErrorMessages.moreThanMaxValue)(max, 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);
}
}

return undefined;
Expand Down
50 changes: 30 additions & 20 deletions packages/x/tests/useDecimalField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { act, renderHook, waitFor } from '@testing-library/react';

import {
DecimalFieldConfig,
defaultErrorMessages,
defaultFormat,
defaultInvalidInputError,
defaultMaxValueError,
defaultMinValueError,
defaultPrecision,
defaultRequiredError,
useDecimalField,
} from '../src/useDecimalField';

Expand Down Expand Up @@ -58,23 +61,23 @@ describe('Decimal field', () => {
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput);
expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError);
});

await act(() => {
result.current.onTextChange('a0');
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput);
expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError);
});

await act(() => {
result.current.onTextChange('hello');
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput);
expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError);
});

await act(() => {
Expand Down Expand Up @@ -150,7 +153,7 @@ describe('Decimal field', () => {
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required);
expect(result.current.meta.error?.$error).toBe(defaultRequiredError);
});
});

Expand All @@ -162,9 +165,15 @@ describe('Decimal field', () => {
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBe(
defaultErrorMessages.lessThanMinValue(0.5, defaultPrecision),
);
expect(result.current.meta.error?.$error).toBe(defaultMinValueError(0.5, defaultPrecision));
});

act(() => {
result.current.control.setValue(0.5);
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBeUndefined();
});
});

Expand All @@ -176,17 +185,21 @@ describe('Decimal field', () => {
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBe(
defaultErrorMessages.moreThanMaxValue(0.5, defaultPrecision),
);
expect(result.current.meta.error?.$error).toBe(defaultMaxValueError(0.5, defaultPrecision));
});

act(() => {
result.current.control.setValue(0.5);
});

await waitFor(() => {
expect(result.current.meta.error?.$error).toBeUndefined();
});
});

it('Should set custom conversion error correctly', async () => {
const [{ result }] = renderUseDecimalField({
errorMessages: {
invalidInput: 'custom',
},
invalidInput: 'custom',
});

await act(() => {
Expand Down Expand Up @@ -216,8 +229,7 @@ describe('Decimal field', () => {

it('Should set custom error if field is required and empty', async () => {
const [{ result }] = renderUseDecimalField({
required: true,
errorMessages: { required: 'custom' },
required: 'custom',
});

act(() => {
Expand All @@ -231,8 +243,7 @@ describe('Decimal field', () => {

it('Should set custom error if field value is less than min', async () => {
const [{ result }] = renderUseDecimalField({
min: 0.5,
errorMessages: { lessThanMinValue: () => 'custom' },
min: [0.5, 'custom'],
});

act(() => {
Expand All @@ -246,8 +257,7 @@ describe('Decimal field', () => {

it('Should set custom error if field value is more than max', async () => {
const [{ result }] = renderUseDecimalField({
max: 0.5,
errorMessages: { moreThanMaxValue: () => 'custom' },
max: [0.5, 'custom'],
});

act(() => {
Expand Down

0 comments on commit 581c7cc

Please sign in to comment.