Skip to content

Commit

Permalink
Extracted StringFieldI18nContext
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexShukel committed Sep 12, 2023
1 parent a6e9d71 commit 44c3fcc
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 47 deletions.
26 changes: 26 additions & 0 deletions packages/x/src/StringFieldI18n.tsx
Original file line number Diff line number Diff line change
@@ -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<StringFieldI18n>(defaultStringFieldI18n);

export type StringFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial<StringFieldI18n> }>;

export const StringFieldI18nContextProvider = ({ i18n, children }: StringFieldI18nContextProviderProps) => {
return (
<StringFieldI18nContext.Provider value={merge(defaultStringFieldI18n, i18n)}>
{children}
</StringFieldI18nContext.Provider>
);
};
45 changes: 12 additions & 33 deletions packages/x/src/useStringField.ts
Original file line number Diff line number Diff line change
@@ -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<T> = [value: T, message: string | ((value: T) => string)];
import { StringFieldI18nContext } from './StringFieldI18n';

export type StringFieldConfig = FieldConfig<string | undefined | null> & {
required?: boolean | string;
minLength?: number | ErrorTuple<number>;
maxLength?: number | ErrorTuple<number>;
required?: boolean;
minLength?: number;
maxLength?: number;
};

export type StringFieldBag = FieldContext<string | undefined | null> & {
Expand All @@ -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;
Expand Down
47 changes: 33 additions & 14 deletions packages/x/tests/useStringField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<StringFieldConfig, 'name'> & {
initialValue?: string;
i18n?: Partial<StringFieldI18n>;
};

const renderUseStringField = (config: Config = {}) => {
const { initialValue = '', ...initialProps } = config;
const { initialValue = '', i18n, ...initialProps } = config;

const formBag = renderHook(() =>
useForm({
Expand All @@ -27,7 +29,9 @@ const renderUseStringField = (config: Config = {}) => {
}),
{
wrapper: ({ children }) => (
<ReactiveFormProvider formBag={formBag.result.current}>{children}</ReactiveFormProvider>
<ReactiveFormProvider formBag={formBag.result.current}>
<StringFieldI18nContextProvider i18n={i18n}>{children}</StringFieldI18nContextProvider>
</ReactiveFormProvider>
),
initialProps,
},
Expand Down Expand Up @@ -57,31 +61,31 @@ describe('String field', () => {
});

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

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

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

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

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

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

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

act(() => {
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand Down

0 comments on commit 44c3fcc

Please sign in to comment.