From 29116ffc17d4467c3873daa275de0ef50d910391 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Fri, 25 Aug 2023 15:09:18 +0300 Subject: [PATCH 01/70] Created `useConverterField` hook --- packages/x/package.json | 10 ++- packages/x/src/useConverterField.ts | 65 ++++++++++++++++++ packages/x/tests/useConverterField.test.tsx | 74 +++++++++++++++++++++ packages/x/tsconfig.json | 2 +- pnpm-lock.yaml | 36 +++++++--- 5 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 packages/x/src/useConverterField.ts create mode 100644 packages/x/tests/useConverterField.test.tsx diff --git a/packages/x/package.json b/packages/x/package.json index 2bcccb22..9519a9a2 100644 --- a/packages/x/package.json +++ b/packages/x/package.json @@ -29,12 +29,16 @@ "@babel/core": "7.19.6", "@reactive-forms/core": "workspace:*", "@reactive-tools/eslint-config": "workspace:*", + "@testing-library/react": "13.4.0", "@types/jest": "26.0.24", + "@types/lodash": "4.14.161", "@types/react": "18.0.23", "aqu": "0.4.3", "jest": "29.2.2", "react": "18.2.0", "rimraf": "3.0.2", + "ts-jest": "29.0.3", + "tslib": "2.3.1", "typescript": "4.8.4" }, "peerDependencies": { @@ -44,5 +48,9 @@ "files": [ "dist" ], - "source": "src/index.ts" + "source": "src/index.ts", + "dependencies": { + "lodash": "4.17.21", + "pxth": "0.6.0" + } } diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts new file mode 100644 index 00000000..39099548 --- /dev/null +++ b/packages/x/src/useConverterField.ts @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import { FieldConfig, FieldContext, FieldError, useField } from '@reactive-forms/core'; +import isObject from 'lodash/isObject'; + +export class ConversionError extends Error { + public constructor(errorMessage: string) { + super(errorMessage); + } +} + +export type ConverterFieldConfig = { + parse: (value: string) => T; // can throw + format: (value: T) => string; // cannot throw + + onChangeText?: (text: string) => void; +} & FieldConfig; + +export type ConverterFieldBag = { + text: string; + onTextChange: (text: string) => void; +} & FieldContext; + +export const useConverterField = ({ + parse, + format, + onChangeText, + ...fieldConfig +}: ConverterFieldConfig): ConverterFieldBag => { + const fieldBag = useField(fieldConfig); + + const { + value, + control: { setValue, setError }, + } = fieldBag; + + const [text, setText] = useState(() => format(value)); + + const tryConvert = (text: string) => { + try { + setValue(parse(text)); + // setHasConversionError(false); + } catch (error) { + if (isObject(error) && error instanceof ConversionError) { + // setHasConversionError(true); + setError({ + $error: error.message, + } as FieldError); + } else { + throw error; + } + } + }; + + const onTextChange = (newText: string) => { + setText(newText); + tryConvert(newText); + onChangeText?.(newText); + }; + + return { + text, + onTextChange, + ...fieldBag, + }; +}; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx new file mode 100644 index 00000000..1172efb8 --- /dev/null +++ b/packages/x/tests/useConverterField.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook } from '@testing-library/react'; + +import { ConversionError, useConverterField } from '../src/useConverterField'; + +const renderUseConverterField = () => { + const { result: formBag } = renderHook(() => + useForm({ + initialValues: { + test: 0, + }, + }), + ); + + const { result: converterFieldBag } = renderHook( + () => + useConverterField({ + parse: (text) => { + const parsingResult = Number.parseInt(text); + + if (Number.isNaN(parsingResult)) { + throw new ConversionError('hello'); + } + + return parsingResult; + }, + format: (value) => String(value), + name: formBag.current.paths.test, + }), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + + return { + formBag, + converterFieldBag, + }; +}; + +describe('Converter field', () => { + it('Should update field with valid value', async () => { + const { converterFieldBag } = renderUseConverterField(); + const { onTextChange } = converterFieldBag.current; + + expect(converterFieldBag.current.value).toBe(0); + expect(converterFieldBag.current.text).toBe('0'); + + await act(async () => { + await onTextChange('1'); + }); + + expect(converterFieldBag.current.value).toBe(1); + expect(converterFieldBag.current.text).toBe('1'); + }); + + it('Should set an error if conversion fails', async () => { + const { converterFieldBag } = renderUseConverterField(); + const { onTextChange } = converterFieldBag.current; + + await act(async () => { + await onTextChange('a'); + }); + + expect(converterFieldBag.current.meta.error?.$error).toBe('hello'); + expect(converterFieldBag.current.value).toBe(0); + expect(converterFieldBag.current.text).toBe('a'); + }); + + it('Should update text when form value changes', async () => {}); +}); diff --git a/packages/x/tsconfig.json b/packages/x/tsconfig.json index f7dfac1c..a027e04d 100644 --- a/packages/x/tsconfig.json +++ b/packages/x/tsconfig.json @@ -21,5 +21,5 @@ "strict": true, "importHelpers": true }, - "include": ["src", "types"] + "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f428aa2b..0aede1f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,23 +157,36 @@ importers: '@babel/core': 7.19.6 '@reactive-forms/core': workspace:* '@reactive-tools/eslint-config': workspace:* + '@testing-library/react': 13.4.0 '@types/jest': 26.0.24 + '@types/lodash': 4.14.161 '@types/react': 18.0.23 aqu: 0.4.3 jest: 29.2.2 + lodash: 4.17.21 + pxth: 0.6.0 react: 18.2.0 rimraf: 3.0.2 + ts-jest: 29.0.3 + tslib: 2.3.1 typescript: 4.8.4 + dependencies: + lodash: 4.17.21 + pxth: 0.6.0 devDependencies: '@babel/core': 7.19.6 '@reactive-forms/core': link:../core '@reactive-tools/eslint-config': link:../../tools/eslint-config + '@testing-library/react': 13.4.0_react@18.2.0 '@types/jest': 26.0.24 + '@types/lodash': 4.14.161 '@types/react': 18.0.23 aqu: 0.4.3_@babel+core@7.19.6 jest: 29.2.2 react: 18.2.0 rimraf: 3.0.2 + ts-jest: 29.0.3_fxe5mizohawh6cjruma5lyyrna + tslib: 2.3.1 typescript: 4.8.4 publishDirectory: prepared-package @@ -3228,7 +3241,7 @@ packages: engines: {node: '>=12'} dependencies: '@babel/code-frame': 7.18.6 - '@babel/runtime': 7.19.4 + '@babel/runtime': 7.20.7 '@types/aria-query': 4.2.2 aria-query: 5.1.1 chalk: 4.1.2 @@ -3251,6 +3264,19 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: true + /@testing-library/react/13.4.0_react@18.2.0: + resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.20.7 + '@testing-library/dom': 8.19.0 + '@types/react-dom': 18.0.7 + react: 18.2.0 + dev: true + /@tootallnate/once/2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -9211,7 +9237,7 @@ packages: '@babel/core': 7.19.6 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.2.2_@types+node@18.11.18 + jest: 29.2.2 jest-util: 29.2.1 json5: 2.2.1 lodash.memoize: 4.1.2 @@ -9433,12 +9459,6 @@ packages: engines: {node: '>=4.2.0'} hasBin: true - /typescript/5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: From 3b753e698135bfbe5b760547688752f9ebc8292a Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Fri, 25 Aug 2023 16:30:22 +0300 Subject: [PATCH 02/70] Synchronize form value with text state --- packages/x/src/index.ts | 1 + packages/x/src/useConverterField.ts | 25 ++++++++++++---- packages/x/tests/useConverterField.test.tsx | 33 ++++++++++++++++++++- packages/x/tsconfig.json | 2 +- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/x/src/index.ts b/packages/x/src/index.ts index 1110b645..8eb012ea 100644 --- a/packages/x/src/index.ts +++ b/packages/x/src/index.ts @@ -1 +1,2 @@ export * from './plugin'; +export * from './useConverterField'; diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 39099548..a93cd237 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { FieldConfig, FieldContext, FieldError, useField } from '@reactive-forms/core'; import isObject from 'lodash/isObject'; @@ -9,8 +9,8 @@ export class ConversionError extends Error { } export type ConverterFieldConfig = { - parse: (value: string) => T; // can throw - format: (value: T) => string; // cannot throw + parse: (value: string) => T; + format: (value: T) => string; onChangeText?: (text: string) => void; } & FieldConfig; @@ -34,14 +34,18 @@ export const useConverterField = ({ } = fieldBag; const [text, setText] = useState(() => format(value)); + const textRef = useRef(text); + textRef.current = text; + + const [hasConversionError, setHasConversionError] = useState(false); const tryConvert = (text: string) => { try { setValue(parse(text)); - // setHasConversionError(false); + setHasConversionError(false); } catch (error) { if (isObject(error) && error instanceof ConversionError) { - // setHasConversionError(true); + setHasConversionError(true); setError({ $error: error.message, } as FieldError); @@ -52,11 +56,22 @@ export const useConverterField = ({ }; const onTextChange = (newText: string) => { + textRef.current = newText; setText(newText); tryConvert(newText); onChangeText?.(newText); }; + useEffect(() => { + if (hasConversionError) { + return; + } + + const formattedValue = format(value); + textRef.current = formattedValue; + setText(formattedValue); + }, [value, format, hasConversionError]); + return { text, onTextChange, diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 1172efb8..2f039e23 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -70,5 +70,36 @@ describe('Converter field', () => { expect(converterFieldBag.current.text).toBe('a'); }); - it('Should update text when form value changes', async () => {}); + it('Should update text when form value changes', async () => { + const { converterFieldBag, formBag } = renderUseConverterField(); + + const { paths } = formBag.current; + + await act(async () => { + await formBag.current.setFieldValue(paths.test, 1); + }); + + expect(converterFieldBag.current.value).toBe(1); + expect(converterFieldBag.current.text).toBe('1'); + }); + + it('should clear conversion error', async () => { + const { converterFieldBag } = renderUseConverterField(); + + const { onTextChange } = converterFieldBag.current; + + await act(async () => { + await onTextChange('a'); + }); + + expect(converterFieldBag.current.meta.error?.$error).toBe('hello'); + + await act(async () => { + await onTextChange('1'); + }); + + expect(converterFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(converterFieldBag.current.value).toBe(1); + expect(converterFieldBag.current.text).toBe('1'); + }); }); diff --git a/packages/x/tsconfig.json b/packages/x/tsconfig.json index a027e04d..97f79bdb 100644 --- a/packages/x/tsconfig.json +++ b/packages/x/tsconfig.json @@ -21,5 +21,5 @@ "strict": true, "importHelpers": true }, - "include": ["src"] + "include": ["src", "tests"] } From d008ef83eaa788a037ee6ac516b9134eea51ad38 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Fri, 25 Aug 2023 16:31:16 +0300 Subject: [PATCH 03/70] Created changeset --- .changeset/four-turkeys-kiss.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-turkeys-kiss.md diff --git a/.changeset/four-turkeys-kiss.md b/.changeset/four-turkeys-kiss.md new file mode 100644 index 00000000..a9693875 --- /dev/null +++ b/.changeset/four-turkeys-kiss.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': patch +--- + +Created useConverterField hook in @reactive-forms/x package From 95da53a2b1d1764be5531eff428ad42244092266 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Fri, 25 Aug 2023 16:43:19 +0300 Subject: [PATCH 04/70] Added more tests --- packages/x/tests/useConverterField.test.tsx | 51 ++++++++++++++++----- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 2f039e23..aa61532c 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -4,7 +4,17 @@ import { act, renderHook } from '@testing-library/react'; import { ConversionError, useConverterField } from '../src/useConverterField'; -const renderUseConverterField = () => { +const defaultParse = (text: string) => { + const parsingResult = Number.parseInt(text); + + if (Number.isNaN(parsingResult)) { + throw new ConversionError('hello'); + } + + return parsingResult; +}; + +const renderUseConverterField = (parse: (value: string) => number = defaultParse) => { const { result: formBag } = renderHook(() => useForm({ initialValues: { @@ -16,15 +26,7 @@ const renderUseConverterField = () => { const { result: converterFieldBag } = renderHook( () => useConverterField({ - parse: (text) => { - const parsingResult = Number.parseInt(text); - - if (Number.isNaN(parsingResult)) { - throw new ConversionError('hello'); - } - - return parsingResult; - }, + parse, format: (value) => String(value), name: formBag.current.paths.test, }), @@ -83,7 +85,7 @@ describe('Converter field', () => { expect(converterFieldBag.current.text).toBe('1'); }); - it('should clear conversion error', async () => { + it('Should clear conversion error', async () => { const { converterFieldBag } = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; @@ -102,4 +104,31 @@ describe('Converter field', () => { expect(converterFieldBag.current.value).toBe(1); expect(converterFieldBag.current.text).toBe('1'); }); + + it('Should rethrow an error in case it is not ConversionError', () => { + const { converterFieldBag } = renderUseConverterField(() => { + throw new Error('custom'); + }); + + act(() => { + expect(() => converterFieldBag.current.onTextChange('')).toThrow(); + }); + }); + + it('Should not update text if there are some conversion errors', async () => { + const { converterFieldBag, formBag } = renderUseConverterField(); + const { onTextChange } = converterFieldBag.current; + const { setFieldValue, paths } = formBag.current; + + await act(async () => { + await onTextChange('a'); + }); + + await act(async () => { + await setFieldValue(paths.test, 1); + }); + + expect(converterFieldBag.current.value).toBe(1); + expect(converterFieldBag.current.text).toBe('a'); + }); }); From da1df9bcc0a3ea29e2c908c9b4db975370654598 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Fri, 25 Aug 2023 17:43:33 +0300 Subject: [PATCH 05/70] Added validator in ConverterField --- packages/x/src/useConverterField.ts | 22 +++++++++++++++++++-- packages/x/tests/useConverterField.test.tsx | 15 ++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index a93cd237..6746ae14 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { FieldConfig, FieldContext, FieldError, useField } from '@reactive-forms/core'; +import { FieldConfig, FieldContext, FieldError, useField, useFieldValidator } from '@reactive-forms/core'; import isObject from 'lodash/isObject'; export class ConversionError extends Error { @@ -41,7 +41,8 @@ export const useConverterField = ({ const tryConvert = (text: string) => { try { - setValue(parse(text)); + const value = parse(text); // this could throw in case of conversion error + setValue(value); setHasConversionError(false); } catch (error) { if (isObject(error) && error instanceof ConversionError) { @@ -62,6 +63,23 @@ export const useConverterField = ({ onChangeText?.(newText); }; + useFieldValidator({ + name: fieldConfig.name, + validator: () => { + try { + parse(textRef.current); + } catch (error) { + if (isObject(error) && error instanceof ConversionError) { + return error.message; + } + + throw error; + } + + return undefined; + }, + }); + useEffect(() => { if (hasConversionError) { return; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index aa61532c..33fdb855 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -131,4 +131,19 @@ describe('Converter field', () => { expect(converterFieldBag.current.value).toBe(1); expect(converterFieldBag.current.text).toBe('a'); }); + + it('Should return error from validator', async () => { + const { converterFieldBag, formBag } = renderUseConverterField(); + + const { onTextChange } = converterFieldBag.current; + const { validateForm, values } = formBag.current; + + await act(async () => { + await onTextChange('a'); + }); + + const errors = await validateForm(values.getValues()); + + expect(errors.test?.$error).toBe('hello'); + }); }); From cb6f7622318324a2325af3237e0b39475c45b9fb Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Fri, 25 Aug 2023 19:17:33 +0300 Subject: [PATCH 06/70] Handled one more test case --- packages/x/src/useConverterField.ts | 22 ++++++++++++----- packages/x/tests/useConverterField.test.tsx | 26 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 6746ae14..1231f1a5 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -11,19 +11,18 @@ export class ConversionError extends Error { export type ConverterFieldConfig = { parse: (value: string) => T; format: (value: T) => string; - - onChangeText?: (text: string) => void; } & FieldConfig; export type ConverterFieldBag = { text: string; onTextChange: (text: string) => void; + onFocus: () => void; + onBlur: () => void; } & FieldContext; export const useConverterField = ({ parse, format, - onChangeText, ...fieldConfig }: ConverterFieldConfig): ConverterFieldBag => { const fieldBag = useField(fieldConfig); @@ -33,6 +32,7 @@ export const useConverterField = ({ control: { setValue, setError }, } = fieldBag; + const [isFocused, setIsFocused] = useState(false); const [text, setText] = useState(() => format(value)); const textRef = useRef(text); textRef.current = text; @@ -60,7 +60,15 @@ export const useConverterField = ({ textRef.current = newText; setText(newText); tryConvert(newText); - onChangeText?.(newText); + }; + + const onFocus = () => { + setIsFocused(true); + }; + + const onBlur = () => { + setIsFocused(false); + tryConvert(text); }; useFieldValidator({ @@ -81,18 +89,20 @@ export const useConverterField = ({ }); useEffect(() => { - if (hasConversionError) { + if (isFocused || hasConversionError) { return; } const formattedValue = format(value); textRef.current = formattedValue; setText(formattedValue); - }, [value, format, hasConversionError]); + }, [value, format, hasConversionError, isFocused]); return { text, onTextChange, + onFocus, + onBlur, ...fieldBag, }; }; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 33fdb855..071b96b0 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -146,4 +146,30 @@ describe('Converter field', () => { expect(errors.test?.$error).toBe('hello'); }); + + // TODO: tricky test case, maybe behavior can change + it('Should ignore new value when field is focused and set old value when field is blurred', async () => { + const { converterFieldBag, formBag } = renderUseConverterField(); + + const { onFocus, onBlur } = converterFieldBag.current; + const { setFieldValue, paths } = formBag.current; + + await act(async () => { + await onFocus(); + }); + + await act(async () => { + await setFieldValue(paths.test, 1); + }); + + expect(converterFieldBag.current.text).toBe('0'); + expect(converterFieldBag.current.value).toBe(1); + + await act(async () => { + await onBlur(); + }); + + expect(converterFieldBag.current.text).toBe('0'); + expect(converterFieldBag.current.value).toBe(0); + }); }); From fe213ed044dde248aaefb8b80c8624a647dd8fc7 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 11:27:30 +0300 Subject: [PATCH 07/70] Set field touched=true on blur --- packages/x/src/useConverterField.ts | 5 +++-- packages/x/tests/useConverterField.test.tsx | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 1231f1a5..2a92df9e 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { FieldConfig, FieldContext, FieldError, useField, useFieldValidator } from '@reactive-forms/core'; +import { FieldConfig, FieldContext, FieldError, FieldTouched, useField, useFieldValidator } from '@reactive-forms/core'; import isObject from 'lodash/isObject'; export class ConversionError extends Error { @@ -29,7 +29,7 @@ export const useConverterField = ({ const { value, - control: { setValue, setError }, + control: { setValue, setError, setTouched }, } = fieldBag; const [isFocused, setIsFocused] = useState(false); @@ -68,6 +68,7 @@ export const useConverterField = ({ const onBlur = () => { setIsFocused(false); + setTouched({ $touched: true } as FieldTouched); tryConvert(text); }; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 071b96b0..03587848 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -172,4 +172,16 @@ describe('Converter field', () => { expect(converterFieldBag.current.text).toBe('0'); expect(converterFieldBag.current.value).toBe(0); }); + + it('Should set field touched=true on blur', async () => { + const { converterFieldBag } = renderUseConverterField(); + + const { onBlur } = converterFieldBag.current; + + await act(async () => { + await onBlur(); + }); + + expect(converterFieldBag.current.meta.touched?.$touched).toBe(true); + }); }); From 6e6cab1f26db2c1c030c0555dfbaf2461a2a0f89 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 11:47:38 +0300 Subject: [PATCH 08/70] Created new option "ignoreFormStateUpdatesWhileFocus" --- packages/x/src/useConverterField.ts | 8 +++- packages/x/tests/useConverterField.test.tsx | 41 ++++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 2a92df9e..65d397f2 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -11,6 +11,9 @@ export class ConversionError extends Error { export type ConverterFieldConfig = { parse: (value: string) => T; format: (value: T) => string; + + // An option that allow to ignore updates incoming from form level state while field is focused + ignoreFormStateUpdatesWhileFocus?: boolean; } & FieldConfig; export type ConverterFieldBag = { @@ -23,6 +26,7 @@ export type ConverterFieldBag = { export const useConverterField = ({ parse, format, + ignoreFormStateUpdatesWhileFocus, ...fieldConfig }: ConverterFieldConfig): ConverterFieldBag => { const fieldBag = useField(fieldConfig); @@ -90,14 +94,14 @@ export const useConverterField = ({ }); useEffect(() => { - if (isFocused || hasConversionError) { + if ((isFocused && ignoreFormStateUpdatesWhileFocus) || hasConversionError) { return; } const formattedValue = format(value); textRef.current = formattedValue; setText(formattedValue); - }, [value, format, hasConversionError, isFocused]); + }, [value, format, hasConversionError, isFocused, ignoreFormStateUpdatesWhileFocus]); return { text, diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 03587848..5e56b7bb 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -14,7 +14,14 @@ const defaultParse = (text: string) => { return parsingResult; }; -const renderUseConverterField = (parse: (value: string) => number = defaultParse) => { +type Config = { + parse?: (value: string) => number; + ignoreFormStateUpdatesWhileFocus?: boolean; +}; + +const renderUseConverterField = (config: Config = {}) => { + const { parse = defaultParse, ignoreFormStateUpdatesWhileFocus = false } = config; + const { result: formBag } = renderHook(() => useForm({ initialValues: { @@ -29,6 +36,7 @@ const renderUseConverterField = (parse: (value: string) => number = defaultParse parse, format: (value) => String(value), name: formBag.current.paths.test, + ignoreFormStateUpdatesWhileFocus, }), { wrapper: ({ children }) => ( @@ -106,8 +114,10 @@ describe('Converter field', () => { }); it('Should rethrow an error in case it is not ConversionError', () => { - const { converterFieldBag } = renderUseConverterField(() => { - throw new Error('custom'); + const { converterFieldBag } = renderUseConverterField({ + parse: () => { + throw new Error('custom'); + }, }); act(() => { @@ -147,9 +157,10 @@ describe('Converter field', () => { expect(errors.test?.$error).toBe('hello'); }); - // TODO: tricky test case, maybe behavior can change - it('Should ignore new value when field is focused and set old value when field is blurred', async () => { - const { converterFieldBag, formBag } = renderUseConverterField(); + it('Should ignore new value when field is focused and set old value when field is blurred (with option "ignoreFormStateUpdatesWhileFocus=true")', async () => { + const { converterFieldBag, formBag } = renderUseConverterField({ + ignoreFormStateUpdatesWhileFocus: true, + }); const { onFocus, onBlur } = converterFieldBag.current; const { setFieldValue, paths } = formBag.current; @@ -173,6 +184,24 @@ describe('Converter field', () => { expect(converterFieldBag.current.value).toBe(0); }); + it('Should set new value immediately when field is focused (with option "ignoreFormStateUpdatesWhileFocus=false")', async () => { + const { converterFieldBag, formBag } = renderUseConverterField(); + + const { onFocus } = converterFieldBag.current; + const { setFieldValue, paths } = formBag.current; + + await act(async () => { + await onFocus(); + }); + + await act(async () => { + await setFieldValue(paths.test, 1); + }); + + expect(converterFieldBag.current.text).toBe('1'); + expect(converterFieldBag.current.value).toBe(1); + }); + it('Should set field touched=true on blur', async () => { const { converterFieldBag } = renderUseConverterField(); From 424da596f82485abd3c7c00abaffe508358ff13e Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 13:21:42 +0300 Subject: [PATCH 09/70] Removed unused dependency --- packages/x/package.json | 3 +-- pnpm-lock.yaml | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/x/package.json b/packages/x/package.json index 46ffc8c4..98ed835f 100644 --- a/packages/x/package.json +++ b/packages/x/package.json @@ -48,7 +48,6 @@ ], "source": "src/index.ts", "dependencies": { - "lodash": "4.17.21", - "pxth": "0.6.0" + "lodash": "4.17.21" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c714d5f..06ae7316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,9 +221,6 @@ importers: lodash: specifier: 4.17.21 version: 4.17.21 - pxth: - specifier: 0.6.0 - version: 0.6.0 devDependencies: '@babel/core': specifier: 7.19.6 From 2c62e2da2add316e74922b7579557d11a75bcddc Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 13:37:07 +0300 Subject: [PATCH 10/70] Created forceSetValue function in useConverterField --- packages/x/src/useConverterField.ts | 14 ++++--- packages/x/tests/useConverterField.test.tsx | 42 ++++++++++----------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 65d397f2..116167c2 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -11,9 +11,6 @@ export class ConversionError extends Error { export type ConverterFieldConfig = { parse: (value: string) => T; format: (value: T) => string; - - // An option that allow to ignore updates incoming from form level state while field is focused - ignoreFormStateUpdatesWhileFocus?: boolean; } & FieldConfig; export type ConverterFieldBag = { @@ -26,7 +23,6 @@ export type ConverterFieldBag = { export const useConverterField = ({ parse, format, - ignoreFormStateUpdatesWhileFocus, ...fieldConfig }: ConverterFieldConfig): ConverterFieldBag => { const fieldBag = useField(fieldConfig); @@ -76,6 +72,11 @@ export const useConverterField = ({ tryConvert(text); }; + const forceSetValue = (value: T) => { + onTextChange(format(value)); + setValue(value); + }; + useFieldValidator({ name: fieldConfig.name, validator: () => { @@ -94,14 +95,14 @@ export const useConverterField = ({ }); useEffect(() => { - if ((isFocused && ignoreFormStateUpdatesWhileFocus) || hasConversionError) { + if (isFocused || hasConversionError) { return; } const formattedValue = format(value); textRef.current = formattedValue; setText(formattedValue); - }, [value, format, hasConversionError, isFocused, ignoreFormStateUpdatesWhileFocus]); + }, [value, format, hasConversionError, isFocused]); return { text, @@ -109,5 +110,6 @@ export const useConverterField = ({ onFocus, onBlur, ...fieldBag, + control: { ...fieldBag.control, setValue: forceSetValue }, }; }; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 5e56b7bb..0413704e 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -16,11 +16,10 @@ const defaultParse = (text: string) => { type Config = { parse?: (value: string) => number; - ignoreFormStateUpdatesWhileFocus?: boolean; }; const renderUseConverterField = (config: Config = {}) => { - const { parse = defaultParse, ignoreFormStateUpdatesWhileFocus = false } = config; + const { parse = defaultParse } = config; const { result: formBag } = renderHook(() => useForm({ @@ -36,7 +35,6 @@ const renderUseConverterField = (config: Config = {}) => { parse, format: (value) => String(value), name: formBag.current.paths.test, - ignoreFormStateUpdatesWhileFocus, }), { wrapper: ({ children }) => ( @@ -157,10 +155,8 @@ describe('Converter field', () => { expect(errors.test?.$error).toBe('hello'); }); - it('Should ignore new value when field is focused and set old value when field is blurred (with option "ignoreFormStateUpdatesWhileFocus=true")', async () => { - const { converterFieldBag, formBag } = renderUseConverterField({ - ignoreFormStateUpdatesWhileFocus: true, - }); + it('Should ignore new value when field is focused and set old value when field is blurred', async () => { + const { converterFieldBag, formBag } = renderUseConverterField(); const { onFocus, onBlur } = converterFieldBag.current; const { setFieldValue, paths } = formBag.current; @@ -184,33 +180,35 @@ describe('Converter field', () => { expect(converterFieldBag.current.value).toBe(0); }); - it('Should set new value immediately when field is focused (with option "ignoreFormStateUpdatesWhileFocus=false")', async () => { - const { converterFieldBag, formBag } = renderUseConverterField(); - - const { onFocus } = converterFieldBag.current; - const { setFieldValue, paths } = formBag.current; + it('Should set field touched=true on blur', async () => { + const { converterFieldBag } = renderUseConverterField(); - await act(async () => { - await onFocus(); - }); + const { onBlur } = converterFieldBag.current; await act(async () => { - await setFieldValue(paths.test, 1); + await onBlur(); }); - expect(converterFieldBag.current.text).toBe('1'); - expect(converterFieldBag.current.value).toBe(1); + expect(converterFieldBag.current.meta.touched?.$touched).toBe(true); }); - it('Should set field touched=true on blur', async () => { + it('Should set value both in form state and local text state', async () => { const { converterFieldBag } = renderUseConverterField(); - const { onBlur } = converterFieldBag.current; + const { + control: { setValue }, + onFocus, + } = converterFieldBag.current; await act(async () => { - await onBlur(); + await onFocus(); }); - expect(converterFieldBag.current.meta.touched?.$touched).toBe(true); + await act(async () => { + await setValue(1); + }); + + expect(converterFieldBag.current.value).toBe(1); + expect(converterFieldBag.current.text).toBe('1'); }); }); From abfe761c2505142c70508fc603a82b99f6b5bcd7 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 13:38:35 +0300 Subject: [PATCH 11/70] Wrap all functions with useCallback in useConverterField --- packages/x/src/useConverterField.ts | 69 ++++++++++++++++------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 116167c2..53dd1a6b 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { FieldConfig, FieldContext, FieldError, FieldTouched, useField, useFieldValidator } from '@reactive-forms/core'; import isObject from 'lodash/isObject'; @@ -39,43 +39,52 @@ export const useConverterField = ({ const [hasConversionError, setHasConversionError] = useState(false); - const tryConvert = (text: string) => { - try { - const value = parse(text); // this could throw in case of conversion error - setValue(value); - setHasConversionError(false); - } catch (error) { - if (isObject(error) && error instanceof ConversionError) { - setHasConversionError(true); - setError({ - $error: error.message, - } as FieldError); - } else { - throw error; + const tryConvert = useCallback( + (text: string) => { + try { + const value = parse(text); // this could throw in case of conversion error + setValue(value); + setHasConversionError(false); + } catch (error) { + if (isObject(error) && error instanceof ConversionError) { + setHasConversionError(true); + setError({ + $error: error.message, + } as FieldError); + } else { + throw error; + } } - } - }; - - const onTextChange = (newText: string) => { - textRef.current = newText; - setText(newText); - tryConvert(newText); - }; + }, + [parse, setError, setValue], + ); + + const onTextChange = useCallback( + (newText: string) => { + textRef.current = newText; + setText(newText); + tryConvert(newText); + }, + [tryConvert], + ); - const onFocus = () => { + const onFocus = useCallback(() => { setIsFocused(true); - }; + }, []); - const onBlur = () => { + const onBlur = useCallback(() => { setIsFocused(false); setTouched({ $touched: true } as FieldTouched); tryConvert(text); - }; + }, [setTouched, text, tryConvert]); - const forceSetValue = (value: T) => { - onTextChange(format(value)); - setValue(value); - }; + const forceSetValue = useCallback( + (value: T) => { + onTextChange(format(value)); + setValue(value); + }, + [format, onTextChange, setValue], + ); useFieldValidator({ name: fieldConfig.name, From 787e6569475e13ca0eae0499a8a10095f2b20645 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 13:50:36 +0300 Subject: [PATCH 12/70] Added test case for changing format function --- packages/x/tests/useConverterField.test.tsx | 77 ++++++++++++++++----- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 0413704e..4490e4b4 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -14,14 +14,17 @@ const defaultParse = (text: string) => { return parsingResult; }; +const defaultFormat = (value: number) => String(value); + type Config = { parse?: (value: string) => number; + format?: (value: number) => string; }; const renderUseConverterField = (config: Config = {}) => { - const { parse = defaultParse } = config; + const { parse = defaultParse, format = defaultFormat } = config; - const { result: formBag } = renderHook(() => + const formBag = renderHook(() => useForm({ initialValues: { test: 0, @@ -29,17 +32,23 @@ const renderUseConverterField = (config: Config = {}) => { }), ); - const { result: converterFieldBag } = renderHook( - () => + type Props = Required; + + const converterFieldBag = renderHook( + ({ format, parse }: Props) => useConverterField({ parse, - format: (value) => String(value), - name: formBag.current.paths.test, + format, + name: formBag.result.current.paths.test, }), { wrapper: ({ children }) => ( - {children} + {children} ), + initialProps: { + format, + parse, + }, }, ); @@ -51,7 +60,9 @@ const renderUseConverterField = (config: Config = {}) => { describe('Converter field', () => { it('Should update field with valid value', async () => { - const { converterFieldBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + } = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; expect(converterFieldBag.current.value).toBe(0); @@ -66,7 +77,9 @@ describe('Converter field', () => { }); it('Should set an error if conversion fails', async () => { - const { converterFieldBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + } = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; await act(async () => { @@ -79,7 +92,10 @@ describe('Converter field', () => { }); it('Should update text when form value changes', async () => { - const { converterFieldBag, formBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + formBag: { result: formBag }, + } = renderUseConverterField(); const { paths } = formBag.current; @@ -92,7 +108,9 @@ describe('Converter field', () => { }); it('Should clear conversion error', async () => { - const { converterFieldBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + } = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; @@ -112,7 +130,9 @@ describe('Converter field', () => { }); it('Should rethrow an error in case it is not ConversionError', () => { - const { converterFieldBag } = renderUseConverterField({ + const { + converterFieldBag: { result: converterFieldBag }, + } = renderUseConverterField({ parse: () => { throw new Error('custom'); }, @@ -124,7 +144,10 @@ describe('Converter field', () => { }); it('Should not update text if there are some conversion errors', async () => { - const { converterFieldBag, formBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + formBag: { result: formBag }, + } = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; const { setFieldValue, paths } = formBag.current; @@ -141,7 +164,10 @@ describe('Converter field', () => { }); it('Should return error from validator', async () => { - const { converterFieldBag, formBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + formBag: { result: formBag }, + } = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; const { validateForm, values } = formBag.current; @@ -156,7 +182,10 @@ describe('Converter field', () => { }); it('Should ignore new value when field is focused and set old value when field is blurred', async () => { - const { converterFieldBag, formBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + formBag: { result: formBag }, + } = renderUseConverterField(); const { onFocus, onBlur } = converterFieldBag.current; const { setFieldValue, paths } = formBag.current; @@ -181,7 +210,9 @@ describe('Converter field', () => { }); it('Should set field touched=true on blur', async () => { - const { converterFieldBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + } = renderUseConverterField(); const { onBlur } = converterFieldBag.current; @@ -193,7 +224,9 @@ describe('Converter field', () => { }); it('Should set value both in form state and local text state', async () => { - const { converterFieldBag } = renderUseConverterField(); + const { + converterFieldBag: { result: converterFieldBag }, + } = renderUseConverterField(); const { control: { setValue }, @@ -211,4 +244,14 @@ describe('Converter field', () => { expect(converterFieldBag.current.value).toBe(1); expect(converterFieldBag.current.text).toBe('1'); }); + + it('Should reformat value when format function changes', () => { + const { converterFieldBag } = renderUseConverterField(); + + const format = jest.fn(() => 'test'); + + converterFieldBag.rerender({ format, parse: defaultParse }); + + expect(converterFieldBag.result.current.text).toBe('test'); + }); }); From c8fa353ebf70c6218fe948b3196669b94333d974 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 14:00:23 +0300 Subject: [PATCH 13/70] Created test for changing parse function --- packages/x/src/useConverterField.ts | 10 ++++++++++ packages/x/tests/useConverterField.test.tsx | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index 53dd1a6b..c0fdc236 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -113,6 +113,16 @@ export const useConverterField = ({ setText(formattedValue); }, [value, format, hasConversionError, isFocused]); + const tryConvertRef = useRef(tryConvert); + + useEffect(() => { + if (tryConvertRef.current !== tryConvert) { + tryConvert(textRef.current); // Parse text again when parse function changes + } + + tryConvertRef.current = tryConvert; + }, [tryConvert]); + return { text, onTextChange, diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 4490e4b4..1a6e87cb 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -250,8 +250,22 @@ describe('Converter field', () => { const format = jest.fn(() => 'test'); - converterFieldBag.rerender({ format, parse: defaultParse }); + act(() => { + converterFieldBag.rerender({ format, parse: defaultParse }); + }); expect(converterFieldBag.result.current.text).toBe('test'); }); + + it('Should parse text again when parse function changes', () => { + const { converterFieldBag } = renderUseConverterField(); + + const parse = jest.fn(() => 1); + + act(() => { + converterFieldBag.rerender({ format: defaultFormat, parse }); + }); + + expect(converterFieldBag.result.current.value).toBe(1); + }); }); From 4f4936f84ef7366b4d4fea98aaa9845149327a05 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sat, 26 Aug 2023 14:04:29 +0300 Subject: [PATCH 14/70] Fixed entrypoint config in package.json --- packages/x/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/x/package.json b/packages/x/package.json index 98ed835f..52c5daab 100644 --- a/packages/x/package.json +++ b/packages/x/package.json @@ -3,8 +3,8 @@ "description": "Advanced Reactive Forms components for rich eXperience", "version": "0.10.2", "main": "dist/index.js", - "module": "dist/core.esm.js", - "types": "dist/core.d.ts", + "module": "dist/x.esm.js", + "types": "dist/x.d.ts", "bugs": "https://github.com/fracht/reactive-forms/issues", "homepage": "https://github.com/fracht/reactive-forms#readme", "repository": "fracht/reactive-forms.git", From 6301354e3f38c19befaa1ddbd95db9e9b705a65e Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sun, 27 Aug 2023 16:57:12 +0300 Subject: [PATCH 15/70] Created useIntegerField hook --- packages/x/src/useIntegerField.ts | 108 +++++++++++++++++ packages/x/tests/useConverterField.test.tsx | 53 ++------ packages/x/tests/useIntegerField.test.tsx | 127 ++++++++++++++++++++ 3 files changed, 248 insertions(+), 40 deletions(-) create mode 100644 packages/x/src/useIntegerField.ts create mode 100644 packages/x/tests/useIntegerField.test.tsx diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts new file mode 100644 index 00000000..0b3df82e --- /dev/null +++ b/packages/x/src/useIntegerField.ts @@ -0,0 +1,108 @@ +import { useCallback } from 'react'; +import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; + +import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; + +const INTEGER_REGEX = /^-?\d+$/; + +const formatInteger = (value: number | null | undefined) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toFixed(0); +}; + +export type IntegerFieldErrorMessages = { + invalidInput: string; + required: string; + lessThanMinValue: ((min: number) => string) | string; + moreThanMaxValue: ((max: number) => string) | string; +}; + +export const defaultErrorMessages: IntegerFieldErrorMessages = { + invalidInput: 'Must be integer', + required: 'Field is required', + lessThanMinValue: (min) => `Value should not be less than ${min.toFixed(0)}`, + moreThanMaxValue: (max) => `Value should not be more than ${max.toFixed(0)}`, +}; + +export type IntegerFieldConfig = FieldConfig & { + required?: boolean; + min?: number; + max?: number; + + formatValue?: (value: number | null | undefined) => string; + errorMessages?: IntegerFieldErrorMessages; +}; + +export type IntegerFieldBag = ConverterFieldBag & {}; + +export const useIntegerField = ({ + name, + validator, + schema, + required, + min, + max, + formatValue, + errorMessages = defaultErrorMessages, +}: IntegerFieldConfig): IntegerFieldBag => { + const { invalidInput, required: requiredError, lessThanMinValue, moreThanMaxValue } = errorMessages; + + const parseInteger = useCallback( + (text: string) => { + text = text.trim(); + + if (text.length === 0) { + return null; + } + + if (!INTEGER_REGEX.test(text)) { + throw new ConversionError(invalidInput); + } + + const value = Number.parseInt(text); + + if (Number.isNaN(value)) { + throw new ConversionError(invalidInput); + } + + return value; + }, + [invalidInput], + ); + + const integerBag = useConverterField({ + parse: parseInteger, + format: formatValue ?? formatInteger, + name, + validator, + schema, + }); + + useFieldValidator({ + name, + validator: (value) => { + if (required && typeof value !== 'number') { + return requiredError; + } + + if (typeof value !== 'number') { + return undefined; + } + + if (typeof min === 'number' && value < Math.round(min)) { + return typeof lessThanMinValue === 'function' ? lessThanMinValue(min) : lessThanMinValue; + } + + if (typeof max === 'number' && value > Math.round(max)) { + return typeof moreThanMaxValue === 'function' ? moreThanMaxValue(max) : moreThanMaxValue; + } + + return undefined; + }, + }); + + return integerBag; +}; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/x/tests/useConverterField.test.tsx index 1a6e87cb..eea70f02 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/x/tests/useConverterField.test.tsx @@ -52,17 +52,12 @@ const renderUseConverterField = (config: Config = {}) => { }, ); - return { - formBag, - converterFieldBag, - }; + return [converterFieldBag, formBag] as const; }; describe('Converter field', () => { it('Should update field with valid value', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }] = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; expect(converterFieldBag.current.value).toBe(0); @@ -77,9 +72,7 @@ describe('Converter field', () => { }); it('Should set an error if conversion fails', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }] = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; await act(async () => { @@ -92,10 +85,7 @@ describe('Converter field', () => { }); it('Should update text when form value changes', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - formBag: { result: formBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }, { result: formBag }] = renderUseConverterField(); const { paths } = formBag.current; @@ -108,9 +98,7 @@ describe('Converter field', () => { }); it('Should clear conversion error', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }] = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; @@ -130,9 +118,7 @@ describe('Converter field', () => { }); it('Should rethrow an error in case it is not ConversionError', () => { - const { - converterFieldBag: { result: converterFieldBag }, - } = renderUseConverterField({ + const [{ result: converterFieldBag }] = renderUseConverterField({ parse: () => { throw new Error('custom'); }, @@ -144,10 +130,7 @@ describe('Converter field', () => { }); it('Should not update text if there are some conversion errors', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - formBag: { result: formBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }, { result: formBag }] = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; const { setFieldValue, paths } = formBag.current; @@ -164,10 +147,7 @@ describe('Converter field', () => { }); it('Should return error from validator', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - formBag: { result: formBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }, { result: formBag }] = renderUseConverterField(); const { onTextChange } = converterFieldBag.current; const { validateForm, values } = formBag.current; @@ -182,10 +162,7 @@ describe('Converter field', () => { }); it('Should ignore new value when field is focused and set old value when field is blurred', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - formBag: { result: formBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }, { result: formBag }] = renderUseConverterField(); const { onFocus, onBlur } = converterFieldBag.current; const { setFieldValue, paths } = formBag.current; @@ -210,9 +187,7 @@ describe('Converter field', () => { }); it('Should set field touched=true on blur', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }] = renderUseConverterField(); const { onBlur } = converterFieldBag.current; @@ -224,9 +199,7 @@ describe('Converter field', () => { }); it('Should set value both in form state and local text state', async () => { - const { - converterFieldBag: { result: converterFieldBag }, - } = renderUseConverterField(); + const [{ result: converterFieldBag }] = renderUseConverterField(); const { control: { setValue }, @@ -246,7 +219,7 @@ describe('Converter field', () => { }); it('Should reformat value when format function changes', () => { - const { converterFieldBag } = renderUseConverterField(); + const [converterFieldBag] = renderUseConverterField(); const format = jest.fn(() => 'test'); @@ -258,7 +231,7 @@ describe('Converter field', () => { }); it('Should parse text again when parse function changes', () => { - const { converterFieldBag } = renderUseConverterField(); + const [converterFieldBag] = renderUseConverterField(); const parse = jest.fn(() => 1); diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx new file mode 100644 index 00000000..8c86fa83 --- /dev/null +++ b/packages/x/tests/useIntegerField.test.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { defaultErrorMessages, IntegerFieldConfig, useIntegerField } from '../src/useIntegerField'; + +type Config = Omit & { + initialValue?: number | null; +}; + +const renderUseIntegerField = (config: Config = {}) => { + const { initialValue = 0, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const integerFieldBag = renderHook( + (props: Omit) => + useIntegerField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps, + }, + ); + + return [integerFieldBag, formBag] as const; +}; + +describe('Integer field', () => { + it('Should format initial value correctly', () => { + const [{ result: integerFieldBag }] = renderUseIntegerField(); + + expect(integerFieldBag.current.text).toBe('0'); + expect(integerFieldBag.current.value).toBe(0); + }); + + it('Should set default error in case of conversion error and clear it afterwards', async () => { + const [{ result: integerFieldBag }] = renderUseIntegerField(); + + act(() => { + integerFieldBag.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + act(() => { + integerFieldBag.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if text was not parsed successfully', async () => { + const [{ result: integerFieldBag }] = renderUseIntegerField(); + + act(() => { + integerFieldBag.current.onTextChange('a'); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ required: true }); + + act(() => { + formBag.current.setFieldValue(formBag.current.paths.test, null); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + }); + + // FIXME: enable this test after fixing useFieldValidator + // it.skip('Should set validate field on initial render', async () => { + // const [{ result: integerFieldBag }] = renderUseIntegerField({ required: true, initialValue: null }); + + // expect(integerFieldBag.current.meta.error?.$error).toBe( + // (defaultErrorMessages.lessThanMinValue as (min: number) => string)(1), + // ); + // }); + + it('Should set default error if field value is less than min', async () => { + const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ min: 0 }); + + act(() => { + formBag.current.setFieldValue(formBag.current.paths.test, -1); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe( + (defaultErrorMessages.lessThanMinValue as (min: number) => string)(0), + ); + }); + }); + + it('Should set default error if field value is more than max', async () => { + const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ max: 0 }); + + act(() => { + formBag.current.setFieldValue(formBag.current.paths.test, 1); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe( + (defaultErrorMessages.moreThanMaxValue as (max: number) => string)(0), + ); + }); + }); +}); From 1b207812748eb77ecae674b525f4b9836f53c794 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Sun, 27 Aug 2023 17:33:15 +0300 Subject: [PATCH 16/70] 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 17/70] 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 18/70] 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 19/70] 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 20/70] 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 a2732f5126395141120677ff3c5a60537223e98f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 12:25:59 +0300 Subject: [PATCH 21/70] Added tests for custom errors in useIntegerField --- packages/x/src/useIntegerField.ts | 22 +++--- packages/x/tests/useIntegerField.test.tsx | 95 ++++++++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index 0b3df82e..2df65a81 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -16,8 +16,8 @@ const formatInteger = (value: number | null | undefined) => { export type IntegerFieldErrorMessages = { invalidInput: string; required: string; - lessThanMinValue: ((min: number) => string) | string; - moreThanMaxValue: ((max: number) => string) | string; + lessThanMinValue: (min: number) => string; + moreThanMaxValue: (max: number) => string; }; export const defaultErrorMessages: IntegerFieldErrorMessages = { @@ -33,7 +33,7 @@ export type IntegerFieldConfig = FieldConfig & { max?: number; formatValue?: (value: number | null | undefined) => string; - errorMessages?: IntegerFieldErrorMessages; + errorMessages?: Partial; }; export type IntegerFieldBag = ConverterFieldBag & {}; @@ -48,8 +48,6 @@ export const useIntegerField = ({ formatValue, errorMessages = defaultErrorMessages, }: IntegerFieldConfig): IntegerFieldBag => { - const { invalidInput, required: requiredError, lessThanMinValue, moreThanMaxValue } = errorMessages; - const parseInteger = useCallback( (text: string) => { text = text.trim(); @@ -58,19 +56,21 @@ export const useIntegerField = ({ return null; } + const errorMessage = errorMessages.invalidInput ?? defaultErrorMessages.invalidInput; + if (!INTEGER_REGEX.test(text)) { - throw new ConversionError(invalidInput); + throw new ConversionError(errorMessage); } const value = Number.parseInt(text); if (Number.isNaN(value)) { - throw new ConversionError(invalidInput); + throw new ConversionError(errorMessage); } return value; }, - [invalidInput], + [errorMessages.invalidInput], ); const integerBag = useConverterField({ @@ -85,7 +85,7 @@ export const useIntegerField = ({ name, validator: (value) => { if (required && typeof value !== 'number') { - return requiredError; + return errorMessages.required ?? defaultErrorMessages.required; } if (typeof value !== 'number') { @@ -93,11 +93,11 @@ export const useIntegerField = ({ } if (typeof min === 'number' && value < Math.round(min)) { - return typeof lessThanMinValue === 'function' ? lessThanMinValue(min) : lessThanMinValue; + return (errorMessages.lessThanMinValue ?? defaultErrorMessages.lessThanMinValue)(min); } if (typeof max === 'number' && value > Math.round(max)) { - return typeof moreThanMaxValue === 'function' ? moreThanMaxValue(max) : moreThanMaxValue; + return (errorMessages.moreThanMaxValue ?? defaultErrorMessages.moreThanMaxValue)(max); } return undefined; diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index 8c86fa83..4bc4e3ca 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -44,6 +44,15 @@ describe('Integer field', () => { expect(integerFieldBag.current.value).toBe(0); }); + // FIXME: enable this test after fixing useFieldValidator + // it.skip('Should set validate field on initial render', async () => { + // const [{ result: integerFieldBag }] = renderUseIntegerField({ required: true, initialValue: null }); + + // expect(integerFieldBag.current.meta.error?.$error).toBe( + // (defaultErrorMessages.lessThanMinValue as (min: number) => string)(1), + // ); + // }); + it('Should set default error in case of conversion error and clear it afterwards', async () => { const [{ result: integerFieldBag }] = renderUseIntegerField(); @@ -88,15 +97,6 @@ describe('Integer field', () => { }); }); - // FIXME: enable this test after fixing useFieldValidator - // it.skip('Should set validate field on initial render', async () => { - // const [{ result: integerFieldBag }] = renderUseIntegerField({ required: true, initialValue: null }); - - // expect(integerFieldBag.current.meta.error?.$error).toBe( - // (defaultErrorMessages.lessThanMinValue as (min: number) => string)(1), - // ); - // }); - it('Should set default error if field value is less than min', async () => { const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ min: 0 }); @@ -124,4 +124,81 @@ describe('Integer field', () => { ); }); }); + + it('Should set custom error in case of conversion error and clear it afterwards', async () => { + const [{ result: integerFieldBag }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); + + act(() => { + integerFieldBag.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + integerFieldBag.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if text was not parsed successfully', async () => { + const [{ result: integerFieldBag }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); + + act(() => { + integerFieldBag.current.onTextChange('a'); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ + required: true, + errorMessages: { required: 'custom' }, + }); + + act(() => { + formBag.current.setFieldValue(formBag.current.paths.test, null); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is less than min', async () => { + const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ + min: 0, + errorMessages: { lessThanMinValue: () => 'custom' }, + }); + + act(() => { + formBag.current.setFieldValue(formBag.current.paths.test, -1); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is more than max', async () => { + const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ + max: 0, + errorMessages: { moreThanMaxValue: () => 'custom' }, + }); + + act(() => { + formBag.current.setFieldValue(formBag.current.paths.test, 1); + }); + + await waitFor(() => { + expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + }); + }); }); From d970e2e938f928690a96829549621e40d75853a7 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 12:37:15 +0300 Subject: [PATCH 22/70] Added test --- packages/x/src/useIntegerField.ts | 4 ++-- packages/x/tests/useIntegerField.test.tsx | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index 2df65a81..b7a59d8b 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -45,7 +45,7 @@ export const useIntegerField = ({ required, min, max, - formatValue, + formatValue = formatInteger, errorMessages = defaultErrorMessages, }: IntegerFieldConfig): IntegerFieldBag => { const parseInteger = useCallback( @@ -75,7 +75,7 @@ export const useIntegerField = ({ const integerBag = useConverterField({ parse: parseInteger, - format: formatValue ?? formatInteger, + format: formatValue, name, validator, schema, diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index 4bc4e3ca..1ff66341 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -201,4 +201,13 @@ describe('Integer field', () => { expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); }); }); + + it('Should be able to format integer differently', () => { + const formatValue = jest.fn(() => 'custom'); + const initialValue = 42; + const [{ result: integerFieldBag }] = renderUseIntegerField({ formatValue, initialValue }); + + expect(integerFieldBag.current.text).toBe('custom'); + expect(formatValue).toBeCalledWith(initialValue); + }); }); From c2d69d4052ad7a8343a1fa791a6c264fbba843a9 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 12:39:37 +0300 Subject: [PATCH 23/70] Added changeset --- .changeset/red-badgers-doubt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-badgers-doubt.md diff --git a/.changeset/red-badgers-doubt.md b/.changeset/red-badgers-doubt.md new file mode 100644 index 00000000..36d892b7 --- /dev/null +++ b/.changeset/red-badgers-doubt.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': minor +--- + +Created useIntegerField hook From 9240ac884a720f560902d252b61b31949ea17f12 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 13:13:00 +0300 Subject: [PATCH 24/70] Created useBooleanField hook --- packages/x/src/useBooleanField.ts | 40 +++++++ packages/x/tests/useBooleanField.test.tsx | 125 ++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 packages/x/src/useBooleanField.ts create mode 100644 packages/x/tests/useBooleanField.test.tsx diff --git a/packages/x/src/useBooleanField.ts b/packages/x/src/useBooleanField.ts new file mode 100644 index 00000000..0c4fe548 --- /dev/null +++ b/packages/x/src/useBooleanField.ts @@ -0,0 +1,40 @@ +import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; + +export type BooleanFieldConfig = FieldConfig & { + required?: boolean; + requiredError?: string; +}; + +export const defaultRequiredError = 'Field is required'; + +export type BooleanFieldBag = FieldContext & { + onBlur: () => void; +}; + +export const useBooleanField = ({ required, requiredError = defaultRequiredError, ...config }: BooleanFieldConfig) => { + const fieldBag = useField(config); + + const { + control: { setTouched }, + } = fieldBag; + + const onBlur = () => { + setTouched({ $touched: true }); + }; + + useFieldValidator({ + name: config.name, + validator: (value) => { + if (required && !value) { + return requiredError; + } + + return undefined; + }, + }); + + return { + ...fieldBag, + onBlur, + }; +}; diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx new file mode 100644 index 00000000..03605b3a --- /dev/null +++ b/packages/x/tests/useBooleanField.test.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { BooleanFieldConfig, defaultRequiredError, useBooleanField } from '../src/useBooleanField'; + +type Config = Omit & { + initialValue?: boolean; +}; + +const renderUseStringField = (config: Config = {}) => { + const { initialValue = false, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const stringFieldBag = renderHook( + (props: Omit) => + useBooleanField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps, + }, + ); + + return [stringFieldBag, formBag] as const; +}; + +describe('Boolean field', () => { + it('Should set touched=true on blur', async () => { + const [{ result }] = renderUseStringField(); + + expect(result.current.meta.touched?.$touched).toBeFalsy(); + + await act(() => { + result.current.onBlur(); + }); + + await waitFor(() => { + expect(result.current.meta.touched?.$touched).toBeTruthy(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseStringField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + }); + + act(() => { + result.current.control.setValue(false); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + }); + + act(() => { + result.current.control.setValue(true); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseStringField({ required: true, requiredError: 'custom' }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(false); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(true); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); +}); From db2d6858f1bcab38c10eeef461f00e8909a7166f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 13:14:40 +0300 Subject: [PATCH 25/70] Added changeset --- .changeset/stale-cars-heal.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-cars-heal.md diff --git a/.changeset/stale-cars-heal.md b/.changeset/stale-cars-heal.md new file mode 100644 index 00000000..fea90e02 --- /dev/null +++ b/.changeset/stale-cars-heal.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': minor +--- + +Created useBooleanField hook From 6fa257ad7bf3dd2f13217dd7c2a2624217ed3003 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:34:22 +0300 Subject: [PATCH 26/70] Created useDecimalField --- .gitignore | 5 +- .vscode/launch.json | 4 +- .vscode/launch.template.json | 15 ++ packages/x/src/useDecimalField.ts | 142 ++++++++++ packages/x/tests/useDecimalField.test.tsx | 312 ++++++++++++++++++++++ 5 files changed, 475 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.template.json create mode 100644 packages/x/src/useDecimalField.ts create mode 100644 packages/x/tests/useDecimalField.test.tsx diff --git a/.gitignore b/.gitignore index 7c73a981..e6e1e2ff 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ yarn-error.log* scripts/*.mjs coverage -**/.turbo \ No newline at end of file +**/.turbo + +# vscode +launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json index b1f33dea..b6f2cad4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ "name": "Debug Core Tests", "type": "node", "request": "launch", - "runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/aqu/dist/aqu.js", "test", "--runInBand"], - "cwd": "${workspaceFolder}/packages/core", + "runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch", "useDecimalField"], + "cwd": "${workspaceFolder}/packages/x", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229 diff --git a/.vscode/launch.template.json b/.vscode/launch.template.json new file mode 100644 index 00000000..e3c68b49 --- /dev/null +++ b/.vscode/launch.template.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Core Tests", + "type": "node", + "request": "launch", + "runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch"], + "cwd": "${workspaceFolder}/packages/core", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "port": 9229 + } + ] +} diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts new file mode 100644 index 00000000..aaeb05e6 --- /dev/null +++ b/packages/x/src/useDecimalField.ts @@ -0,0 +1,142 @@ +import { useCallback } from 'react'; +import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; + +import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; + +const DECIMAL_REGEX = /^\d*\.?\d*$/; + +export const defaultLocales: Intl.LocalesArgument = 'EN'; + +export const defaultFormatOptions: Intl.NumberFormatOptions = { + minimumFractionDigits: 1, + maximumFractionDigits: 2, +}; + +const formatDecimal = ( + value: number | null | undefined, + locales?: Intl.LocalesArgument, + options?: Intl.NumberFormatOptions, +) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toLocaleString(locales, options); +}; + +export type DecimalFieldErrorMessages = { + invalidInput: string; + required: string; + lessThanMinValue: (min: number, locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions) => string; + moreThanMaxValue: (max: number, locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions) => string; +}; + +export const defaultErrorMessages: DecimalFieldErrorMessages = { + invalidInput: 'Must be decimal', + required: 'Field is required', + lessThanMinValue: (min, locales, options) => + `Value should not be less than ${formatDecimal(min, locales, options)}`, + moreThanMaxValue: (max, locales, options) => + `Value should not be more than ${formatDecimal(max, locales, options)}`, +}; + +export type DecimalFieldConfig = FieldConfig & { + required?: boolean; + min?: number; + max?: number; + + formatValue?: (value: number | null | undefined) => string; + errorMessages?: Partial; + + locales?: Intl.LocalesArgument; + formatOptions?: Intl.NumberFormatOptions; +}; + +export type DecimalFieldBag = ConverterFieldBag & {}; + +export const useDecimalField = ({ + name, + validator, + schema, + required, + min, + max, + formatValue, + errorMessages = defaultErrorMessages, + locales = defaultLocales, + formatOptions = defaultFormatOptions, +}: DecimalFieldConfig): DecimalFieldBag => { + const parseDecimal = useCallback( + (text: string) => { + text = text.trim(); + + if (text.length === 0) { + return null; + } + + const errorMessage = errorMessages.invalidInput ?? defaultErrorMessages.invalidInput; + + if (!DECIMAL_REGEX.test(text)) { + throw new ConversionError(errorMessage); + } + + 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(errorMessage); + } + + return value; + }, + [errorMessages.invalidInput], + ); + + const format = useCallback( + (value: number | null | undefined) => { + if (formatValue) { + return formatValue(value); + } + + return formatDecimal(value, locales, formatOptions); + }, + [formatOptions, formatValue, locales], + ); + + const decimalBag = useConverterField({ + parse: parseDecimal, + format, + name, + validator, + schema, + }); + + useFieldValidator({ + name, + validator: (value) => { + if (required && typeof value !== 'number') { + return errorMessages.required ?? defaultErrorMessages.required; + } + + if (typeof value !== 'number') { + return undefined; + } + + if (typeof min === 'number' && value < min) { + return (errorMessages.lessThanMinValue ?? defaultErrorMessages.lessThanMinValue)(min); + } + + if (typeof max === 'number' && value > max) { + return (errorMessages.moreThanMaxValue ?? defaultErrorMessages.moreThanMaxValue)(max); + } + + return undefined; + }, + }); + + return decimalBag; +}; diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx new file mode 100644 index 00000000..b7e4ec67 --- /dev/null +++ b/packages/x/tests/useDecimalField.test.tsx @@ -0,0 +1,312 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + DecimalFieldConfig, + defaultErrorMessages, + defaultFormatOptions, + defaultLocales, + useDecimalField, +} from '../src/useDecimalField'; + +type Config = Omit & { + initialValue?: number | null; +}; + +const renderUseDecimalField = (config: Config = {}) => { + const { initialValue = 0, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const decimalFieldBag = renderHook( + (props: Omit) => + useDecimalField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps, + }, + ); + + return [decimalFieldBag, formBag] as const; +}; + +describe('Decimal field', () => { + it('Should format initial value correctly', () => { + const [{ result }] = renderUseDecimalField(); + + expect(result.current.text).toBe((0).toLocaleString(defaultLocales, defaultFormatOptions)); + expect(result.current.value).toBe(0); + }); + + it('Should set default conversion error correctly', async () => { + const [{ result }] = renderUseDecimalField(); + + await act(() => { + result.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + await act(() => { + result.current.onTextChange('a0'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + await act(() => { + result.current.onTextChange('hello'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + await act(() => { + result.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(''); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(' '); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.text).toBe(' '); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('.'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('.0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('0.'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseDecimalField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + }); + + it('Should set default error if field value is less than min', async () => { + const [{ result }] = renderUseDecimalField({ min: 0.5 }); + + act(() => { + result.current.control.setValue(0.25); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.lessThanMinValue(0.5)); + }); + }); + + it('Should set default error if field value is more than max', async () => { + const [{ result }] = renderUseDecimalField({ max: 0.5 }); + + act(() => { + result.current.control.setValue(0.75); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.moreThanMaxValue(0.5)); + }); + }); + + it('Should set custom conversion error correctly', async () => { + const [{ result }] = renderUseDecimalField({ + errorMessages: { + invalidInput: 'custom', + }, + }); + + await act(() => { + result.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('a0'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('hello'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(''); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(' '); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.text).toBe(' '); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('.'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('.0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('0.'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseDecimalField({ + required: true, + errorMessages: { required: 'custom' }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is less than min', async () => { + const [{ result }] = renderUseDecimalField({ + min: 0.5, + errorMessages: { lessThanMinValue: () => 'custom' }, + }); + + act(() => { + result.current.control.setValue(0.25); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is more than max', async () => { + const [{ result }] = renderUseDecimalField({ + max: 0.5, + errorMessages: { moreThanMaxValue: () => 'custom' }, + }); + + act(() => { + result.current.control.setValue(0.75); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should be able to format decimal differently', () => { + const formatValue = jest.fn(() => 'custom'); + const initialValue = 3.14; + const [{ result }] = renderUseDecimalField({ formatValue, initialValue }); + + expect(result.current.text).toBe('custom'); + expect(formatValue).toBeCalledWith(initialValue); + }); +}); From 2758e02826a7cef7cc7e8f43051eb78f07eefd04 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:35:22 +0300 Subject: [PATCH 27/70] Added changeset --- .changeset/healthy-comics-refuse.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/healthy-comics-refuse.md diff --git a/.changeset/healthy-comics-refuse.md b/.changeset/healthy-comics-refuse.md new file mode 100644 index 00000000..41f993ad --- /dev/null +++ b/.changeset/healthy-comics-refuse.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': minor +--- + +Created useDecimalField hook From 5e8ae6adc6749f2169a2861ca5ed28d4e8b969da Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:40:26 +0300 Subject: [PATCH 28/70] Refactored tests --- packages/x/tests/useIntegerField.test.tsx | 91 ++++++++++------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index 1ff66341..e4cbc137 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -19,7 +19,7 @@ const renderUseIntegerField = (config: Config = {}) => { }), ); - const integerFieldBag = renderHook( + const result = renderHook( (props: Omit) => useIntegerField({ name: formBag.result.current.paths.test, @@ -33,181 +33,172 @@ const renderUseIntegerField = (config: Config = {}) => { }, ); - return [integerFieldBag, formBag] as const; + return [result, formBag] as const; }; describe('Integer field', () => { it('Should format initial value correctly', () => { - const [{ result: integerFieldBag }] = renderUseIntegerField(); + const [{ result }] = renderUseIntegerField(); - expect(integerFieldBag.current.text).toBe('0'); - expect(integerFieldBag.current.value).toBe(0); + expect(result.current.text).toBe('0'); + expect(result.current.value).toBe(0); }); - // FIXME: enable this test after fixing useFieldValidator - // it.skip('Should set validate field on initial render', async () => { - // const [{ result: integerFieldBag }] = renderUseIntegerField({ required: true, initialValue: null }); - - // expect(integerFieldBag.current.meta.error?.$error).toBe( - // (defaultErrorMessages.lessThanMinValue as (min: number) => string)(1), - // ); - // }); - it('Should set default error in case of conversion error and clear it afterwards', async () => { - const [{ result: integerFieldBag }] = renderUseIntegerField(); + const [{ result }] = renderUseIntegerField(); act(() => { - integerFieldBag.current.onTextChange('0a'); + result.current.onTextChange('0a'); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); }); act(() => { - integerFieldBag.current.onTextChange('0'); + result.current.onTextChange('0'); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); it('Should set default error if text was not parsed successfully', async () => { - const [{ result: integerFieldBag }] = renderUseIntegerField(); + const [{ result }] = renderUseIntegerField(); act(() => { - integerFieldBag.current.onTextChange('a'); + result.current.onTextChange('a'); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); }); }); it('Should set default error if field is required and empty', async () => { - const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ required: true }); + const [{ result }] = renderUseIntegerField({ required: true }); act(() => { - formBag.current.setFieldValue(formBag.current.paths.test, null); + result.current.control.setValue(null); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); }); }); it('Should set default error if field value is less than min', async () => { - const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ min: 0 }); + const [{ result }] = renderUseIntegerField({ min: 0 }); act(() => { - formBag.current.setFieldValue(formBag.current.paths.test, -1); + result.current.control.setValue(-1); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe( + expect(result.current.meta.error?.$error).toBe( (defaultErrorMessages.lessThanMinValue as (min: number) => string)(0), ); }); }); it('Should set default error if field value is more than max', async () => { - const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ max: 0 }); + const [{ result }] = renderUseIntegerField({ max: 0 }); act(() => { - formBag.current.setFieldValue(formBag.current.paths.test, 1); + result.current.control.setValue(1); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe( + expect(result.current.meta.error?.$error).toBe( (defaultErrorMessages.moreThanMaxValue as (max: number) => string)(0), ); }); }); it('Should set custom error in case of conversion error and clear it afterwards', async () => { - const [{ result: integerFieldBag }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); + const [{ result }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); act(() => { - integerFieldBag.current.onTextChange('0a'); + result.current.onTextChange('0a'); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); act(() => { - integerFieldBag.current.onTextChange('0'); + result.current.onTextChange('0'); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); it('Should set custom error if text was not parsed successfully', async () => { - const [{ result: integerFieldBag }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); + const [{ result }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); act(() => { - integerFieldBag.current.onTextChange('a'); + result.current.onTextChange('a'); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); }); it('Should set custom error if field is required and empty', async () => { - const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ + const [{ result }] = renderUseIntegerField({ required: true, errorMessages: { required: 'custom' }, }); act(() => { - formBag.current.setFieldValue(formBag.current.paths.test, null); + result.current.control.setValue(null); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); }); it('Should set custom error if field value is less than min', async () => { - const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ + const [{ result }] = renderUseIntegerField({ min: 0, errorMessages: { lessThanMinValue: () => 'custom' }, }); act(() => { - formBag.current.setFieldValue(formBag.current.paths.test, -1); + result.current.control.setValue(-1); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); }); it('Should set custom error if field value is more than max', async () => { - const [{ result: integerFieldBag }, { result: formBag }] = renderUseIntegerField({ + const [{ result }] = renderUseIntegerField({ max: 0, errorMessages: { moreThanMaxValue: () => 'custom' }, }); act(() => { - formBag.current.setFieldValue(formBag.current.paths.test, 1); + result.current.control.setValue(1); }); await waitFor(() => { - expect(integerFieldBag.current.meta.error?.$error).toBe('custom'); + expect(result.current.meta.error?.$error).toBe('custom'); }); }); it('Should be able to format integer differently', () => { const formatValue = jest.fn(() => 'custom'); const initialValue = 42; - const [{ result: integerFieldBag }] = renderUseIntegerField({ formatValue, initialValue }); + const [{ result }] = renderUseIntegerField({ formatValue, initialValue }); - expect(integerFieldBag.current.text).toBe('custom'); + expect(result.current.text).toBe('custom'); expect(formatValue).toBeCalledWith(initialValue); }); }); From 9bbd99c728bac94ace26f7cc4778320919d7f631 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:49:30 +0300 Subject: [PATCH 29/70] Refactoring --- packages/x/src/useDecimalField.ts | 2 +- packages/x/tests/useDecimalField.test.tsx | 64 ++++------------------- 2 files changed, 10 insertions(+), 56 deletions(-) diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index aaeb05e6..5c4a0701 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -52,7 +52,7 @@ export type DecimalFieldConfig = FieldConfig & { formatOptions?: Intl.NumberFormatOptions; }; -export type DecimalFieldBag = ConverterFieldBag & {}; +export type DecimalFieldBag = ConverterFieldBag; export const useDecimalField = ({ name, diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx index b7e4ec67..0648c433 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/x/tests/useDecimalField.test.tsx @@ -131,6 +131,15 @@ describe('Decimal field', () => { expect(result.current.value).toBe(0); expect(result.current.meta.error?.$error).toBeUndefined(); }); + + await act(() => { + result.current.onTextChange('0.0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); }); it('Should set default error if field is required and empty', async () => { @@ -199,61 +208,6 @@ describe('Decimal field', () => { await waitFor(() => { expect(result.current.meta.error?.$error).toBe('custom'); }); - - await act(() => { - result.current.onTextChange('0'); - }); - - await waitFor(() => { - expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBeUndefined(); - }); - - await act(() => { - result.current.onTextChange(''); - }); - - await waitFor(() => { - expect(result.current.value).toBe(null); - expect(result.current.meta.error?.$error).toBeUndefined(); - }); - - await act(() => { - result.current.onTextChange(' '); - }); - - await waitFor(() => { - expect(result.current.value).toBe(null); - expect(result.current.text).toBe(' '); - expect(result.current.meta.error?.$error).toBeUndefined(); - }); - - await act(() => { - result.current.onTextChange('.'); - }); - - await waitFor(() => { - expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBeUndefined(); - }); - - await act(() => { - result.current.onTextChange('.0'); - }); - - await waitFor(() => { - expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBeUndefined(); - }); - - await act(() => { - result.current.onTextChange('0.'); - }); - - await waitFor(() => { - expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBeUndefined(); - }); }); it('Should set custom error if field is required and empty', async () => { From fe963a0292497c7bb57fe482991dd19d278d95f2 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:52:51 +0300 Subject: [PATCH 30/70] Refactoring --- packages/x/tests/useBooleanField.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx index 03605b3a..214f7386 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/x/tests/useBooleanField.test.tsx @@ -8,7 +8,7 @@ type Config = Omit & { initialValue?: boolean; }; -const renderUseStringField = (config: Config = {}) => { +const renderUseBooleanField = (config: Config = {}) => { const { initialValue = false, ...initialProps } = config; const formBag = renderHook(() => @@ -38,7 +38,7 @@ const renderUseStringField = (config: Config = {}) => { describe('Boolean field', () => { it('Should set touched=true on blur', async () => { - const [{ result }] = renderUseStringField(); + const [{ result }] = renderUseBooleanField(); expect(result.current.meta.touched?.$touched).toBeFalsy(); @@ -52,7 +52,7 @@ describe('Boolean field', () => { }); it('Should set default error if field is required and empty', async () => { - const [{ result }] = renderUseStringField({ required: true }); + const [{ result }] = renderUseBooleanField({ required: true }); act(() => { result.current.control.setValue(null); @@ -88,7 +88,7 @@ describe('Boolean field', () => { }); it('Should set custom error if field is required and empty', async () => { - const [{ result }] = renderUseStringField({ required: true, requiredError: 'custom' }); + const [{ result }] = renderUseBooleanField({ required: true, requiredError: 'custom' }); act(() => { result.current.control.setValue(null); From e099312082c789dbda273319926669d8edcb26a7 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 15:57:54 +0300 Subject: [PATCH 31/70] 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 90279ab963742689e5a08b076fb3dcaf673d7c7d Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Mon, 28 Aug 2023 16:06:35 +0300 Subject: [PATCH 32/70] Added possibility to customize parseDecimal function --- packages/x/src/useDecimalField.ts | 4 +++- packages/x/tests/useDecimalField.test.tsx | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index 5c4a0701..12ef172c 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -46,6 +46,7 @@ export type DecimalFieldConfig = FieldConfig & { max?: number; formatValue?: (value: number | null | undefined) => string; + parseDecimal?: (text: string) => number; errorMessages?: Partial; locales?: Intl.LocalesArgument; @@ -62,6 +63,7 @@ export const useDecimalField = ({ min, max, formatValue, + parseDecimal: parseDecimalProps, errorMessages = defaultErrorMessages, locales = defaultLocales, formatOptions = defaultFormatOptions, @@ -108,7 +110,7 @@ export const useDecimalField = ({ ); const decimalBag = useConverterField({ - parse: parseDecimal, + parse: parseDecimalProps ?? parseDecimal, format, name, validator, diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx index 0648c433..996ad0cc 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/x/tests/useDecimalField.test.tsx @@ -263,4 +263,18 @@ describe('Decimal field', () => { expect(result.current.text).toBe('custom'); expect(formatValue).toBeCalledWith(initialValue); }); + + it('Should call custom parseDecimal function', async () => { + const parseDecimal = jest.fn(); + + const [{ result }] = renderUseDecimalField({ parseDecimal }); + + await act(() => { + result.current.onTextChange('0.0'); + }); + + await waitFor(() => { + expect(parseDecimal).toBeCalledWith('0.0'); + }); + }); }); From dc396f815f0c13b0cb4ad2b9bfaaf01e84a3de8f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 29 Aug 2023 11:02:02 +0300 Subject: [PATCH 33/70] Created useDateField --- .vscode/launch.json | 4 +- packages/x/package.json | 1 + packages/x/src/useDateField.ts | 144 +++++++++++++ packages/x/tests/useDateField.test.tsx | 271 +++++++++++++++++++++++++ pnpm-lock.yaml | 7 + 5 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 packages/x/src/useDateField.ts create mode 100644 packages/x/tests/useDateField.test.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index b1f33dea..640cff88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ "name": "Debug Core Tests", "type": "node", "request": "launch", - "runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/aqu/dist/aqu.js", "test", "--runInBand"], - "cwd": "${workspaceFolder}/packages/core", + "runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch", "useDateField"], + "cwd": "${workspaceFolder}/packages/x", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229 diff --git a/packages/x/package.json b/packages/x/package.json index 52c5daab..ec6af61a 100644 --- a/packages/x/package.json +++ b/packages/x/package.json @@ -48,6 +48,7 @@ ], "source": "src/index.ts", "dependencies": { + "dayjs": "^1.11.9", "lodash": "4.17.21" } } diff --git a/packages/x/src/useDateField.ts b/packages/x/src/useDateField.ts new file mode 100644 index 00000000..8c9b9799 --- /dev/null +++ b/packages/x/src/useDateField.ts @@ -0,0 +1,144 @@ +import { useCallback } from 'react'; +import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; +import dayjs from 'dayjs'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; + +import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; + +dayjs.extend(customParseFormat); + +const defaultDateFormats = ['DD.MM.YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD', 'YYYY.MM.DD', 'DD-MM-YYYY', 'DD/MM/YYYY']; +const defaultDateTimeFormats = [ + 'DD.MM.YYYY HH:mm', + 'YYYY-MM-DD HH:mm', + 'YYYY/MM/DD HH:mm', + 'YYYY.MM.DD HH:mm', + 'DD-MM-YYYY HH:mm', + 'DD/MM/YYYY HH:mm', +]; + +export const defaultLocales: Intl.LocalesArgument = 'EN'; + +export const defaultFormatOptions: Intl.DateTimeFormatOptions = {}; + +const formatDate = ( + value: Date | null | undefined, + locales?: Intl.LocalesArgument, + options?: Intl.DateTimeFormatOptions, +) => { + if (!(value instanceof Date)) { + return ''; + } + + return value.toLocaleString(locales, options); +}; + +export type DateFieldErrorMessages = { + invalidInput: string; + required: string; + earlierThanMinDate: (min: Date, locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string; + laterThanMaxDate: (max: Date, locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string; +}; + +export const defaultErrorMessages: DateFieldErrorMessages = { + invalidInput: 'Must be date', + required: 'Field is required', + earlierThanMinDate: (min, locales, options) => `Date must not be earlier than ${formatDate(min, locales, options)}`, + laterThanMaxDate: (max, locales, options) => `Date must not be later than ${formatDate(max, locales, options)}`, +}; + +export type DateFieldConfig = FieldConfig & { + required?: boolean; + minDate?: Date; + maxDate?: Date; + pickTime?: boolean; + + formatDate?: (date: Date | null | undefined) => string; + parseDate?: (text: string) => Date; + errorMessages?: Partial; + + locales?: Intl.LocalesArgument; + formatOptions?: Intl.DateTimeFormatOptions; +}; + +export type DateFieldBag = ConverterFieldBag; + +export const useDateField = ({ + name, + validator, + schema, + required, + minDate, + maxDate, + pickTime, + formatDate: formatDateProps, + parseDate: parseDateProps, + errorMessages = defaultErrorMessages, + locales = defaultLocales, + formatOptions = defaultFormatOptions, +}: DateFieldConfig): DateFieldBag => { + const parseDate = useCallback( + (text: string) => { + text = text.trim(); + + if (text.length === 0) { + return null; + } + + const errorMessage = errorMessages.invalidInput ?? defaultErrorMessages.invalidInput; + + const date = dayjs(text, [...defaultDateFormats, ...(pickTime ? defaultDateTimeFormats : [])], true); + + if (!date.isValid()) { + throw new ConversionError(errorMessage); + } + + return date.toDate(); + }, + [errorMessages.invalidInput, pickTime], + ); + + const format = useCallback( + (value: Date | null | undefined) => { + if (formatDateProps) { + return formatDateProps(value); + } + + return formatDate(value, locales, formatOptions); + }, + [formatDateProps, formatOptions, locales], + ); + + const dateBag = useConverterField({ + parse: parseDateProps ?? parseDate, + format, + name, + validator, + schema, + }); + + useFieldValidator({ + name, + validator: (value) => { + if (required && !(value instanceof Date)) { + return errorMessages.required ?? defaultErrorMessages.required; + } + + if (!(value instanceof Date)) { + return undefined; + } + + if (minDate instanceof Date && dayjs(minDate).diff(dayjs(value)) > 0) { + return (errorMessages.earlierThanMinDate ?? defaultErrorMessages.earlierThanMinDate)(minDate); + } + + if (maxDate instanceof Date && dayjs(value).diff(dayjs(maxDate)) > 0) { + return (errorMessages.laterThanMaxDate ?? defaultErrorMessages.laterThanMaxDate)(maxDate); + } + + return undefined; + }, + }); + + return dateBag; +}; diff --git a/packages/x/tests/useDateField.test.tsx b/packages/x/tests/useDateField.test.tsx new file mode 100644 index 00000000..7ef80a69 --- /dev/null +++ b/packages/x/tests/useDateField.test.tsx @@ -0,0 +1,271 @@ +import React from 'react'; +import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + DateFieldConfig, + defaultErrorMessages, + defaultFormatOptions, + defaultLocales, + useDateField, +} from '../src/useDateField'; + +type Config = Omit & { + initialValue?: Date | null; +}; + +const renderUseDateField = (config: Config = {}) => { + const { initialValue = null, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const dateFieldBag = renderHook( + (props: Omit) => + useDateField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps, + }, + ); + + return [dateFieldBag, formBag] as const; +}; + +describe('Date field', () => { + it.skip('Should format initial value correctly', () => { + const initialValue = new Date(); + + const [{ result }] = renderUseDateField({ initialValue }); + + expect(result.current.text).toBe(initialValue.toLocaleString(defaultLocales, defaultFormatOptions)); + expect(result.current.value?.getTime()).toBe(initialValue.getTime()); + }); + + it('Should set default conversion error correctly', async () => { + const [{ result }] = renderUseDateField(); + + await act(() => { + result.current.onTextChange('2000-20-20'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + await act(() => { + result.current.onTextChange('aaaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + await act(() => { + result.current.onTextChange('1000'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + }); + + await act(() => { + result.current.onTextChange('2003-07-08'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(''); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(' '); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.text).toBe(' '); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('1999/12/12'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it.skip('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseDateField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + }); + }); + + it.skip('Should set default error if date is earlier than minDate', async () => { + const minDate = new Date(2000, 0, 5); + const [{ result }] = renderUseDateField({ minDate }); + + await act(() => { + result.current.control.setValue(new Date(2000, 0, 4)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.earlierThanMinDate(minDate)); + }); + + await act(() => { + result.current.control.setValue(new Date(2000, 0, 6)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it.skip('Should set default error if date is later than maxDate', async () => { + const maxDate = new Date(2000, 0, 5); + const [{ result }] = renderUseDateField({ maxDate }); + + act(() => { + result.current.control.setValue(new Date(2000, 0, 6)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.laterThanMaxDate(maxDate)); + }); + + await act(() => { + result.current.control.setValue(new Date(2000, 0, 4)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it.skip('Should set custom conversion error correctly', async () => { + const [{ result }] = renderUseDateField({ + errorMessages: { + invalidInput: 'custom', + }, + }); + + await act(() => { + result.current.onTextChange('2000-20-20'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('aaaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('1000'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it.skip('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseDateField({ + required: true, + errorMessages: { required: 'custom' }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + // it.skip('Should set custom error if field value is less than min', async () => { + // const [{ result }] = renderUseDateField({ + // min: 0.5, + // errorMessages: { lessThanMinValue: () => 'custom' }, + // }); + + // act(() => { + // result.current.control.setValue(0.25); + // }); + + // await waitFor(() => { + // expect(result.current.meta.error?.$error).toBe('custom'); + // }); + // }); + + // it.skip('Should set custom error if field value is more than max', async () => { + // const [{ result }] = renderUseDateField({ + // max: 0.5, + // errorMessages: { moreThanMaxValue: () => 'custom' }, + // }); + + // act(() => { + // result.current.control.setValue(0.75); + // }); + + // await waitFor(() => { + // expect(result.current.meta.error?.$error).toBe('custom'); + // }); + // }); + + // it.skip('Should be able to format decimal differently', () => { + // const formatValue = jest.fn(() => 'custom'); + // const initialValue = 3.14; + // const [{ result }] = renderUseDateField({ formatValue, initialValue }); + + // expect(result.current.text).toBe('custom'); + // expect(formatValue).toBeCalledWith(initialValue); + // }); + + // it.skip('Should call custom parseDecimal function', async () => { + // const parseDecimal = jest.fn(); + + // const [{ result }] = renderUseDateField({ parseDecimal }); + + // await act(() => { + // result.current.onTextChange('0.0'); + // }); + + // await waitFor(() => { + // expect(parseDecimal).toBeCalledWith('0.0'); + // }); + // }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06ae7316..920df028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -218,6 +218,9 @@ importers: packages/x: dependencies: + dayjs: + specifier: ^1.11.9 + version: 1.11.9 lodash: specifier: 4.17.21 version: 4.17.21 @@ -3845,6 +3848,10 @@ packages: whatwg-url: 11.0.0 dev: true + /dayjs@1.11.9: + resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} + dev: false + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: From 191422f506317c9e8f7daba527651a0948f53dcc Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 29 Aug 2023 17:55:15 +0300 Subject: [PATCH 34/70] Refactored useDecimalField --- packages/x/src/useDecimalField.ts | 60 ++++++++--------------- packages/x/tests/useDecimalField.test.tsx | 31 +++++++----- 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index 12ef172c..8c89dff2 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -4,40 +4,28 @@ import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; const DECIMAL_REGEX = /^\d*\.?\d*$/; +export const defaultPrecision = 2; -export const defaultLocales: Intl.LocalesArgument = 'EN'; - -export const defaultFormatOptions: Intl.NumberFormatOptions = { - minimumFractionDigits: 1, - maximumFractionDigits: 2, -}; - -const formatDecimal = ( - value: number | null | undefined, - locales?: Intl.LocalesArgument, - options?: Intl.NumberFormatOptions, -) => { +export const defaultFormat = (value: number | null | undefined, precision: number) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return ''; } - return value.toLocaleString(locales, options); + return value.toFixed(precision).toString(); }; export type DecimalFieldErrorMessages = { invalidInput: string; required: string; - lessThanMinValue: (min: number, locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions) => string; - moreThanMaxValue: (max: number, locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions) => string; + lessThanMinValue: (min: number, precision: number) => string; + moreThanMaxValue: (max: number, precision: number) => string; }; export const defaultErrorMessages: DecimalFieldErrorMessages = { invalidInput: 'Must be decimal', required: 'Field is required', - lessThanMinValue: (min, locales, options) => - `Value should not be less than ${formatDecimal(min, locales, options)}`, - moreThanMaxValue: (max, locales, options) => - `Value should not be more than ${formatDecimal(max, locales, options)}`, + 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 DecimalFieldConfig = FieldConfig & { @@ -45,12 +33,11 @@ export type DecimalFieldConfig = FieldConfig & { min?: number; max?: number; - formatValue?: (value: number | null | undefined) => string; - parseDecimal?: (text: string) => number; + format?: (value: number | null | undefined, precision: number) => string; + parse?: (text: string) => number; errorMessages?: Partial; - locales?: Intl.LocalesArgument; - formatOptions?: Intl.NumberFormatOptions; + precision?: number; }; export type DecimalFieldBag = ConverterFieldBag; @@ -62,13 +49,12 @@ export const useDecimalField = ({ required, min, max, - formatValue, - parseDecimal: parseDecimalProps, + format, + parse, errorMessages = defaultErrorMessages, - locales = defaultLocales, - formatOptions = defaultFormatOptions, + precision = defaultPrecision, }: DecimalFieldConfig): DecimalFieldBag => { - const parseDecimal = useCallback( + const defaultParse = useCallback( (text: string) => { text = text.trim(); @@ -98,20 +84,16 @@ export const useDecimalField = ({ [errorMessages.invalidInput], ); - const format = useCallback( + const formatValue = useCallback( (value: number | null | undefined) => { - if (formatValue) { - return formatValue(value); - } - - return formatDecimal(value, locales, formatOptions); + return (format ?? defaultFormat)(value, precision); }, - [formatOptions, formatValue, locales], + [format, precision], ); const decimalBag = useConverterField({ - parse: parseDecimalProps ?? parseDecimal, - format, + parse: parse ?? defaultParse, + format: formatValue, name, validator, schema, @@ -129,11 +111,11 @@ export const useDecimalField = ({ } if (typeof min === 'number' && value < min) { - return (errorMessages.lessThanMinValue ?? defaultErrorMessages.lessThanMinValue)(min); + return (errorMessages.lessThanMinValue ?? defaultErrorMessages.lessThanMinValue)(min, precision); } if (typeof max === 'number' && value > max) { - return (errorMessages.moreThanMaxValue ?? defaultErrorMessages.moreThanMaxValue)(max); + return (errorMessages.moreThanMaxValue ?? defaultErrorMessages.moreThanMaxValue)(max, precision); } return undefined; diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx index 996ad0cc..c8ade33e 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/x/tests/useDecimalField.test.tsx @@ -5,8 +5,8 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { DecimalFieldConfig, defaultErrorMessages, - defaultFormatOptions, - defaultLocales, + defaultFormat, + defaultPrecision, useDecimalField, } from '../src/useDecimalField'; @@ -46,7 +46,7 @@ describe('Decimal field', () => { it('Should format initial value correctly', () => { const [{ result }] = renderUseDecimalField(); - expect(result.current.text).toBe((0).toLocaleString(defaultLocales, defaultFormatOptions)); + expect(result.current.text).toBe(defaultFormat(0, defaultPrecision)); expect(result.current.value).toBe(0); }); @@ -162,7 +162,9 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.lessThanMinValue(0.5)); + expect(result.current.meta.error?.$error).toBe( + defaultErrorMessages.lessThanMinValue(0.5, defaultPrecision), + ); }); }); @@ -174,7 +176,9 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.moreThanMaxValue(0.5)); + expect(result.current.meta.error?.$error).toBe( + defaultErrorMessages.moreThanMaxValue(0.5, defaultPrecision), + ); }); }); @@ -255,26 +259,27 @@ describe('Decimal field', () => { }); }); - it('Should be able to format decimal differently', () => { - const formatValue = jest.fn(() => 'custom'); + it('Should call custom format function', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const format = jest.fn((_value: number | null | undefined) => 'custom'); const initialValue = 3.14; - const [{ result }] = renderUseDecimalField({ formatValue, initialValue }); + const [{ result }] = renderUseDecimalField({ format, initialValue }); expect(result.current.text).toBe('custom'); - expect(formatValue).toBeCalledWith(initialValue); + expect(format.mock.calls[0][0]).toBe(initialValue); }); - it('Should call custom parseDecimal function', async () => { - const parseDecimal = jest.fn(); + it('Should call custom parse function', async () => { + const parse = jest.fn(); - const [{ result }] = renderUseDecimalField({ parseDecimal }); + const [{ result }] = renderUseDecimalField({ parse }); await act(() => { result.current.onTextChange('0.0'); }); await waitFor(() => { - expect(parseDecimal).toBeCalledWith('0.0'); + expect(parse).toBeCalledWith('0.0'); }); }); }); From 428c51f869fde6aef72c6721cad49505cecaf939 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 11:35:56 +0300 Subject: [PATCH 35/70] 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 36/70] 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 3c11ed7f09e04e9936e02ffafa395d330c7ffab3 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:02:27 +0300 Subject: [PATCH 37/70] Refactored IntegerField custom errors API --- packages/x/src/useIntegerField.ts | 65 +++++++++++++---------- packages/x/tests/useIntegerField.test.tsx | 52 +++++++++++------- 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index b7a59d8b..8547d1ce 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -1,8 +1,15 @@ import { useCallback } from 'react'; import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; +import isFunction from 'lodash/isFunction'; +import isNil from 'lodash/isNil'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; +export const defaultRequiredError = 'Field is required'; +export const defaultInvalidInputError = 'Must be integer'; +export const defaultMinValueError = (min: number) => `Value should not be less than ${min.toFixed(0)}`; +export const defaultMaxValueError = (max: number) => `Value should not be more than ${max.toFixed(0)}`; + const INTEGER_REGEX = /^-?\d+$/; const formatInteger = (value: number | null | undefined) => { @@ -13,27 +20,15 @@ const formatInteger = (value: number | null | undefined) => { return value.toFixed(0); }; -export type IntegerFieldErrorMessages = { - invalidInput: string; - required: string; - lessThanMinValue: (min: number) => string; - moreThanMaxValue: (max: number) => string; -}; - -export const defaultErrorMessages: IntegerFieldErrorMessages = { - invalidInput: 'Must be integer', - required: 'Field is required', - lessThanMinValue: (min) => `Value should not be less than ${min.toFixed(0)}`, - moreThanMaxValue: (max) => `Value should not be more than ${max.toFixed(0)}`, -}; +export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; export type IntegerFieldConfig = FieldConfig & { - required?: boolean; - min?: number; - max?: number; + required?: boolean | string; + invalidInput?: string; + min?: number | ErrorTuple; + max?: number | ErrorTuple; formatValue?: (value: number | null | undefined) => string; - errorMessages?: Partial; }; export type IntegerFieldBag = ConverterFieldBag & {}; @@ -43,10 +38,10 @@ export const useIntegerField = ({ validator, schema, required, + invalidInput, min, max, formatValue = formatInteger, - errorMessages = defaultErrorMessages, }: IntegerFieldConfig): IntegerFieldBag => { const parseInteger = useCallback( (text: string) => { @@ -56,21 +51,21 @@ export const useIntegerField = ({ return null; } - const errorMessage = errorMessages.invalidInput ?? defaultErrorMessages.invalidInput; + const parseError = invalidInput ?? defaultInvalidInputError; if (!INTEGER_REGEX.test(text)) { - throw new ConversionError(errorMessage); + throw new ConversionError(parseError); } const value = Number.parseInt(text); if (Number.isNaN(value)) { - throw new ConversionError(errorMessage); + throw new ConversionError(parseError); } return value; }, - [errorMessages.invalidInput], + [invalidInput], ); const integerBag = useConverterField({ @@ -85,19 +80,35 @@ export const useIntegerField = ({ 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 < Math.round(min)) { - return (errorMessages.lessThanMinValue ?? defaultErrorMessages.lessThanMinValue)(min); + 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); + } } - if (typeof max === 'number' && value > Math.round(max)) { - return (errorMessages.moreThanMaxValue ?? defaultErrorMessages.moreThanMaxValue)(max); + 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); + } } return undefined; diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index e4cbc137..4607b918 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -2,7 +2,14 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { defaultErrorMessages, IntegerFieldConfig, useIntegerField } from '../src/useIntegerField'; +import { + defaultInvalidInputError, + defaultMaxValueError, + defaultMinValueError, + defaultRequiredError, + IntegerFieldConfig, + useIntegerField, +} from '../src/useIntegerField'; type Config = Omit & { initialValue?: number | null; @@ -52,7 +59,7 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); act(() => { @@ -72,7 +79,7 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); }); @@ -84,7 +91,7 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultRequiredError); }); }); @@ -96,9 +103,15 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe( - (defaultErrorMessages.lessThanMinValue as (min: number) => string)(0), - ); + expect(result.current.meta.error?.$error).toBe(defaultMinValueError(0)); + }); + + act(() => { + result.current.control.setValue(0); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); @@ -110,14 +123,20 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe( - (defaultErrorMessages.moreThanMaxValue as (max: number) => string)(0), - ); + expect(result.current.meta.error?.$error).toBe(defaultMaxValueError(0)); + }); + + act(() => { + result.current.control.setValue(0); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); it('Should set custom error in case of conversion error and clear it afterwards', async () => { - const [{ result }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); + const [{ result }] = renderUseIntegerField({ invalidInput: 'custom' }); act(() => { result.current.onTextChange('0a'); @@ -137,7 +156,7 @@ describe('Integer field', () => { }); it('Should set custom error if text was not parsed successfully', async () => { - const [{ result }] = renderUseIntegerField({ errorMessages: { invalidInput: 'custom' } }); + const [{ result }] = renderUseIntegerField({ invalidInput: 'custom' }); act(() => { result.current.onTextChange('a'); @@ -150,8 +169,7 @@ describe('Integer field', () => { it('Should set custom error if field is required and empty', async () => { const [{ result }] = renderUseIntegerField({ - required: true, - errorMessages: { required: 'custom' }, + required: 'custom', }); act(() => { @@ -165,8 +183,7 @@ describe('Integer field', () => { it('Should set custom error if field value is less than min', async () => { const [{ result }] = renderUseIntegerField({ - min: 0, - errorMessages: { lessThanMinValue: () => 'custom' }, + min: [0, 'custom'], }); act(() => { @@ -180,8 +197,7 @@ describe('Integer field', () => { it('Should set custom error if field value is more than max', async () => { const [{ result }] = renderUseIntegerField({ - max: 0, - errorMessages: { moreThanMaxValue: () => 'custom' }, + max: [0, 'custom'], }); act(() => { From 95e2d5351273262c22d9a766052a94eab4df8eb4 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:05:59 +0300 Subject: [PATCH 38/70] 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 330ad02aeb32a33fbb881e189c7ba94dca50e1b3 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:09:13 +0300 Subject: [PATCH 39/70] Refactored BooleanField custom errors API --- packages/x/src/useBooleanField.ts | 7 +++---- packages/x/tests/useBooleanField.test.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/x/src/useBooleanField.ts b/packages/x/src/useBooleanField.ts index 0c4fe548..309c73b3 100644 --- a/packages/x/src/useBooleanField.ts +++ b/packages/x/src/useBooleanField.ts @@ -1,8 +1,7 @@ import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; export type BooleanFieldConfig = FieldConfig & { - required?: boolean; - requiredError?: string; + required?: boolean | string; }; export const defaultRequiredError = 'Field is required'; @@ -11,7 +10,7 @@ export type BooleanFieldBag = FieldContext & { onBlur: () => void; }; -export const useBooleanField = ({ required, requiredError = defaultRequiredError, ...config }: BooleanFieldConfig) => { +export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => { const fieldBag = useField(config); const { @@ -26,7 +25,7 @@ export const useBooleanField = ({ required, requiredError = defaultRequiredError name: config.name, validator: (value) => { if (required && !value) { - return requiredError; + return required === true ? defaultRequiredError : required; } return undefined; diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx index 214f7386..70fd2e78 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/x/tests/useBooleanField.test.tsx @@ -88,7 +88,7 @@ describe('Boolean field', () => { }); it('Should set custom error if field is required and empty', async () => { - const [{ result }] = renderUseBooleanField({ required: true, requiredError: 'custom' }); + const [{ result }] = renderUseBooleanField({ required: 'custom' }); act(() => { result.current.control.setValue(null); From 581c7ccfd46ea270c3302eeb7c772d93f7c5726b Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:16:47 +0300 Subject: [PATCH 40/70] Refactored DecimalField custom errors API --- packages/x/src/useDecimalField.ts | 64 ++++++++++++++--------- packages/x/tests/useDecimalField.test.tsx | 50 +++++++++++------- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index 8c89dff2..7d6f725b 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -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'; @@ -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 = [value: T, message: string | ((value: T) => string)]; export type DecimalFieldConfig = FieldConfig & { - required?: boolean; - min?: number; - max?: number; + required?: boolean | string; + invalidInput?: string; + min?: number | ErrorTuple; + max?: number | ErrorTuple; format?: (value: number | null | undefined, precision: number) => string; parse?: (text: string) => number; - errorMessages?: Partial; precision?: number; }; @@ -47,11 +43,11 @@ export const useDecimalField = ({ validator, schema, required, + invalidInput, min, max, format, parse, - errorMessages = defaultErrorMessages, precision = defaultPrecision, }: DecimalFieldConfig): DecimalFieldBag => { const defaultParse = useCallback( @@ -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); @@ -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( @@ -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; diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx index c8ade33e..817acc7b 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/x/tests/useDecimalField.test.tsx @@ -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'; @@ -58,7 +61,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); await act(() => { @@ -66,7 +69,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); await act(() => { @@ -74,7 +77,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); await act(() => { @@ -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); }); }); @@ -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(); }); }); @@ -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(() => { @@ -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(() => { @@ -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(() => { @@ -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(() => { From b1d42cd4688de78392e2f0a32dffd60232375f5f Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 30 Aug 2023 12:24:37 +0300 Subject: [PATCH 41/70] Covered more test cases in useIntegerField --- packages/x/tests/useIntegerField.test.tsx | 27 +++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index 4607b918..56e49137 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -59,27 +59,26 @@ describe('Integer field', () => { }); await waitFor(() => { + expect(result.current.value).toBe(0); expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); act(() => { - result.current.onTextChange('0'); + result.current.onTextChange('a0'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBeUndefined(); + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); }); - }); - - it('Should set default error if text was not parsed successfully', async () => { - const [{ result }] = renderUseIntegerField(); act(() => { - result.current.onTextChange('a'); + result.current.onTextChange('1'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); + expect(result.current.value).toBe(1); + expect(result.current.meta.error?.$error).toBeUndefined(); }); }); @@ -147,23 +146,19 @@ describe('Integer field', () => { }); act(() => { - result.current.onTextChange('0'); + result.current.onTextChange('a0'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBeUndefined(); + expect(result.current.meta.error?.$error).toBe('custom'); }); - }); - - it('Should set custom error if text was not parsed successfully', async () => { - const [{ result }] = renderUseIntegerField({ invalidInput: 'custom' }); act(() => { - result.current.onTextChange('a'); + result.current.onTextChange('0'); }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe('custom'); + 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 42/70] 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 43/70] 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 44/70] 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 45/70] 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 46/70] 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 412823a4c44b3378cf86679a2ba33b0543ce1830 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:05:13 +0300 Subject: [PATCH 47/70] Extracted IntegerFieldI18nContext --- packages/x/src/IntegerFieldI18n.tsx | 30 +++++++++++++ packages/x/src/useIntegerField.ts | 51 +++++++---------------- packages/x/tests/useIntegerField.test.tsx | 35 +++++++++++++--- 3 files changed, 74 insertions(+), 42 deletions(-) create mode 100644 packages/x/src/IntegerFieldI18n.tsx diff --git a/packages/x/src/IntegerFieldI18n.tsx b/packages/x/src/IntegerFieldI18n.tsx new file mode 100644 index 00000000..c00734a7 --- /dev/null +++ b/packages/x/src/IntegerFieldI18n.tsx @@ -0,0 +1,30 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export type IntegerFieldI18n = { + required: string; + invalidInput: string; + minValue: (value: number) => string; + maxValue: (value: number) => string; +}; + +export const defaultIntegerFieldI18n: IntegerFieldI18n = { + required: 'Field is required', + invalidInput: 'Must be integer', + minValue: (min: number) => `Value should not be less than ${min.toFixed(0)}`, + maxValue: (max: number) => `Value should not be more than ${max.toFixed(0)}`, +}; + +export const IntegerFieldI18nContext = createContext(defaultIntegerFieldI18n); + +export type IntegerFieldI18nContextProviderProps = PropsWithChildren<{ + i18n?: Partial; +}>; + +export const IntegerFieldI18nContextProvider = ({ children, i18n }: IntegerFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index 8547d1ce..2b4e742c 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -1,8 +1,7 @@ -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; -import isFunction from 'lodash/isFunction'; -import isNil from 'lodash/isNil'; +import { IntegerFieldI18nContext } from './IntegerFieldI18n'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; export const defaultRequiredError = 'Field is required'; @@ -20,13 +19,10 @@ const formatInteger = (value: number | null | undefined) => { return value.toFixed(0); }; -export type ErrorTuple = [value: T, message: string | ((value: T) => string)]; - export type IntegerFieldConfig = FieldConfig & { - required?: boolean | string; - invalidInput?: string; - min?: number | ErrorTuple; - max?: number | ErrorTuple; + required?: boolean; + min?: number; + max?: number; formatValue?: (value: number | null | undefined) => string; }; @@ -38,11 +34,12 @@ export const useIntegerField = ({ validator, schema, required, - invalidInput, min, max, formatValue = formatInteger, }: IntegerFieldConfig): IntegerFieldBag => { + const i18n = useContext(IntegerFieldI18nContext); + const parseInteger = useCallback( (text: string) => { text = text.trim(); @@ -51,21 +48,19 @@ export const useIntegerField = ({ return null; } - const parseError = invalidInput ?? defaultInvalidInputError; - if (!INTEGER_REGEX.test(text)) { - throw new ConversionError(parseError); + throw new ConversionError(i18n.invalidInput); } const value = Number.parseInt(text); if (Number.isNaN(value)) { - throw new ConversionError(parseError); + throw new ConversionError(i18n.invalidInput); } return value; }, - [invalidInput], + [i18n.invalidInput], ); const integerBag = useConverterField({ @@ -80,35 +75,19 @@ export const useIntegerField = ({ 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); - } + if (typeof min === 'number' && value < min) { + return i18n.minValue(min); } - 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); - } + if (typeof max === 'number' && value > max) { + return i18n.maxValue(max); } return undefined; diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index 56e49137..c74cf793 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; +import { IntegerFieldI18n, IntegerFieldI18nContextProvider } from '../src/IntegerFieldI18n'; import { defaultInvalidInputError, defaultMaxValueError, @@ -13,10 +14,11 @@ import { type Config = Omit & { initialValue?: number | null; + i18n?: Partial; }; const renderUseIntegerField = (config: Config = {}) => { - const { initialValue = 0, ...initialProps } = config; + const { initialValue = 0, i18n, ...initialProps } = config; const formBag = renderHook(() => useForm({ @@ -34,7 +36,9 @@ const renderUseIntegerField = (config: Config = {}) => { }), { wrapper: ({ children }) => ( - {children} + + {children} + ), initialProps, }, @@ -135,7 +139,11 @@ describe('Integer field', () => { }); it('Should set custom error in case of conversion error and clear it afterwards', async () => { - const [{ result }] = renderUseIntegerField({ invalidInput: 'custom' }); + const [{ result }] = renderUseIntegerField({ + i18n: { + invalidInput: 'custom', + }, + }); act(() => { result.current.onTextChange('0a'); @@ -164,7 +172,10 @@ describe('Integer field', () => { it('Should set custom error if field is required and empty', async () => { const [{ result }] = renderUseIntegerField({ - required: 'custom', + required: true, + i18n: { + required: 'custom', + }, }); act(() => { @@ -177,8 +188,13 @@ describe('Integer field', () => { }); it('Should set custom error if field value is less than min', async () => { + const minValue = jest.fn(() => 'custom'); + const [{ result }] = renderUseIntegerField({ - min: [0, 'custom'], + min: 0, + i18n: { + minValue, + }, }); act(() => { @@ -187,12 +203,18 @@ describe('Integer field', () => { await waitFor(() => { expect(result.current.meta.error?.$error).toBe('custom'); + expect(minValue).toBeCalledWith(0); }); }); it('Should set custom error if field value is more than max', async () => { + const maxValue = jest.fn(() => 'custom'); + const [{ result }] = renderUseIntegerField({ - max: [0, 'custom'], + max: 0, + i18n: { + maxValue, + }, }); act(() => { @@ -201,6 +223,7 @@ describe('Integer field', () => { await waitFor(() => { expect(result.current.meta.error?.$error).toBe('custom'); + expect(maxValue).toBeCalledWith(0); }); }); From 581cafe0a82d7db0eea49cff87abe4a059c598c9 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:05:48 +0300 Subject: [PATCH 48/70] 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; From e0e5bf9bc6e152603939171f83f7f0074b825f67 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:08:06 +0300 Subject: [PATCH 49/70] Refactoring --- packages/x/src/IntegerFieldI18n.tsx | 2 +- packages/x/src/useIntegerField.ts | 5 ----- packages/x/tests/useIntegerField.test.tsx | 21 +++++++-------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/x/src/IntegerFieldI18n.tsx b/packages/x/src/IntegerFieldI18n.tsx index c00734a7..0a8fe0ee 100644 --- a/packages/x/src/IntegerFieldI18n.tsx +++ b/packages/x/src/IntegerFieldI18n.tsx @@ -12,7 +12,7 @@ export const defaultIntegerFieldI18n: IntegerFieldI18n = { required: 'Field is required', invalidInput: 'Must be integer', minValue: (min: number) => `Value should not be less than ${min.toFixed(0)}`, - maxValue: (max: number) => `Value should not be more than ${max.toFixed(0)}`, + maxValue: (max: number) => `Value should not be greater than ${max.toFixed(0)}`, }; export const IntegerFieldI18nContext = createContext(defaultIntegerFieldI18n); diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index 2b4e742c..53b8189d 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -4,11 +4,6 @@ import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; import { IntegerFieldI18nContext } from './IntegerFieldI18n'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; -export const defaultRequiredError = 'Field is required'; -export const defaultInvalidInputError = 'Must be integer'; -export const defaultMinValueError = (min: number) => `Value should not be less than ${min.toFixed(0)}`; -export const defaultMaxValueError = (max: number) => `Value should not be more than ${max.toFixed(0)}`; - const INTEGER_REGEX = /^-?\d+$/; const formatInteger = (value: number | null | undefined) => { diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index c74cf793..4ee8f569 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -2,15 +2,8 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { IntegerFieldI18n, IntegerFieldI18nContextProvider } from '../src/IntegerFieldI18n'; -import { - defaultInvalidInputError, - defaultMaxValueError, - defaultMinValueError, - defaultRequiredError, - IntegerFieldConfig, - useIntegerField, -} from '../src/useIntegerField'; +import { defaultIntegerFieldI18n, IntegerFieldI18n, IntegerFieldI18nContextProvider } from '../src/IntegerFieldI18n'; +import { IntegerFieldConfig, useIntegerField } from '../src/useIntegerField'; type Config = Omit & { initialValue?: number | null; @@ -64,7 +57,7 @@ describe('Integer field', () => { await waitFor(() => { expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.invalidInput); }); act(() => { @@ -73,7 +66,7 @@ describe('Integer field', () => { await waitFor(() => { expect(result.current.value).toBe(0); - expect(result.current.meta.error?.$error).toBe(defaultInvalidInputError); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.invalidInput); }); act(() => { @@ -94,7 +87,7 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.required); }); }); @@ -106,7 +99,7 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMinValueError(0)); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.minValue(0)); }); act(() => { @@ -126,7 +119,7 @@ describe('Integer field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultMaxValueError(0)); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.maxValue(0)); }); act(() => { From cb622d9c2d09662a96288d45ba24788e27b86d2a Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:14:20 +0300 Subject: [PATCH 50/70] Extracted BooleanFieldI18nContext --- packages/x/src/BooleanFieldI18n.tsx | 22 ++++++++++++++++++++++ packages/x/src/useBooleanField.ts | 11 +++++++---- packages/x/tests/useBooleanField.test.tsx | 23 ++++++++++++++++------- 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 packages/x/src/BooleanFieldI18n.tsx diff --git a/packages/x/src/BooleanFieldI18n.tsx b/packages/x/src/BooleanFieldI18n.tsx new file mode 100644 index 00000000..da548842 --- /dev/null +++ b/packages/x/src/BooleanFieldI18n.tsx @@ -0,0 +1,22 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export type BooleanFieldI18n = { + required: string; +}; + +export const defaultBooleanFieldI18n: BooleanFieldI18n = { + required: 'Field is required', +}; + +export const BooleanFieldI18nContext = createContext(defaultBooleanFieldI18n); + +export type BooleanFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const BooleanFieldI18nContextProvider = ({ i18n, children }: BooleanFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/x/src/useBooleanField.ts b/packages/x/src/useBooleanField.ts index 309c73b3..52575386 100644 --- a/packages/x/src/useBooleanField.ts +++ b/packages/x/src/useBooleanField.ts @@ -1,11 +1,12 @@ +import { useContext } from 'react'; import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; +import { BooleanFieldI18nContext } from './BooleanFieldI18n'; + export type BooleanFieldConfig = FieldConfig & { - required?: boolean | string; + required?: boolean; }; -export const defaultRequiredError = 'Field is required'; - export type BooleanFieldBag = FieldContext & { onBlur: () => void; }; @@ -17,6 +18,8 @@ export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => control: { setTouched }, } = fieldBag; + const i18n = useContext(BooleanFieldI18nContext); + const onBlur = () => { setTouched({ $touched: true }); }; @@ -25,7 +28,7 @@ export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => name: config.name, validator: (value) => { if (required && !value) { - return required === true ? defaultRequiredError : required; + return i18n.required; } return undefined; diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/x/tests/useBooleanField.test.tsx index 70fd2e78..57dabc51 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/x/tests/useBooleanField.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 { BooleanFieldConfig, defaultRequiredError, useBooleanField } from '../src/useBooleanField'; +import { BooleanFieldI18n, BooleanFieldI18nContextProvider, defaultBooleanFieldI18n } from '../src/BooleanFieldI18n'; +import { BooleanFieldConfig, useBooleanField } from '../src/useBooleanField'; type Config = Omit & { initialValue?: boolean; + i18n?: Partial; }; const renderUseBooleanField = (config: Config = {}) => { - const { initialValue = false, ...initialProps } = config; + const { initialValue = false, i18n, ...initialProps } = config; const formBag = renderHook(() => useForm({ @@ -27,7 +29,9 @@ const renderUseBooleanField = (config: Config = {}) => { }), { wrapper: ({ children }) => ( - {children} + + {children} + ), initialProps, }, @@ -59,7 +63,7 @@ describe('Boolean field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); }); act(() => { @@ -67,7 +71,7 @@ describe('Boolean field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); }); act(() => { @@ -75,7 +79,7 @@ describe('Boolean field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultRequiredError); + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); }); act(() => { @@ -88,7 +92,12 @@ describe('Boolean field', () => { }); it('Should set custom error if field is required and empty', async () => { - const [{ result }] = renderUseBooleanField({ required: 'custom' }); + const [{ result }] = renderUseBooleanField({ + required: true, + i18n: { + required: 'custom', + }, + }); act(() => { result.current.control.setValue(null); From a0fa8d236e6aa60a9a8002b47a0e721a773c46e8 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:47:05 +0300 Subject: [PATCH 51/70] Extracted DecimalFieldI18nContext --- packages/x/src/DecimalFieldI18n.tsx | 36 ++++++++++++ packages/x/src/useDecimalField.ts | 70 +++++------------------ packages/x/tests/useDecimalField.test.tsx | 55 +++++++++++------- 3 files changed, 84 insertions(+), 77 deletions(-) create mode 100644 packages/x/src/DecimalFieldI18n.tsx 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(() => { From 88aac09c46ebc903681da6be51693cdbc694ebae Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 16:49:40 +0300 Subject: [PATCH 52/70] Extracted formatDecimal function --- packages/x/src/DecimalFieldI18n.tsx | 12 +++--------- packages/x/src/formatDecimal.ts | 7 +++++++ packages/x/src/useDecimalField.ts | 5 +++-- packages/x/tests/useDecimalField.test.tsx | 10 +++------- 4 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 packages/x/src/formatDecimal.ts diff --git a/packages/x/src/DecimalFieldI18n.tsx b/packages/x/src/DecimalFieldI18n.tsx index f08f59d1..1fae42f0 100644 --- a/packages/x/src/DecimalFieldI18n.tsx +++ b/packages/x/src/DecimalFieldI18n.tsx @@ -1,13 +1,7 @@ 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(); -}; +import { formatDecimal } from './formatDecimal'; export type DecimalFieldI18n = { required: string; @@ -19,8 +13,8 @@ export type DecimalFieldI18n = { 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)}`, + minValue: (min: number, precision: number) => `Value should not be less than ${formatDecimal(min, precision)}`, + maxValue: (max: number, precision: number) => `Value should not be greater than ${formatDecimal(max, precision)}`, }; export const DecimalFieldI18nContext = createContext(defaultDecimalFieldI18n); diff --git a/packages/x/src/formatDecimal.ts b/packages/x/src/formatDecimal.ts new file mode 100644 index 00000000..8da992d3 --- /dev/null +++ b/packages/x/src/formatDecimal.ts @@ -0,0 +1,7 @@ +export const formatDecimal = (value: number | null | undefined, precision: number) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toFixed(precision).toString(); +}; diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index 7ac9a252..98f42961 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -1,7 +1,8 @@ import { useCallback, useContext } from 'react'; import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; -import { DecimalFieldI18nContext, defaultFormat } from './DecimalFieldI18n'; +import { DecimalFieldI18nContext } from './DecimalFieldI18n'; +import { formatDecimal } from './formatDecimal'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; const DECIMAL_REGEX = /^\d*\.?\d*$/; @@ -58,7 +59,7 @@ export const useDecimalField = ({ const formatValue = useCallback( (value: number | null | undefined) => { - return (format ?? defaultFormat)(value, precision); + return (format ?? formatDecimal)(value, precision); }, [format, precision], ); diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/x/tests/useDecimalField.test.tsx index df288b19..4235c366 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/x/tests/useDecimalField.test.tsx @@ -2,12 +2,8 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { - DecimalFieldI18n, - DecimalFieldI18nContextProvider, - defaultDecimalFieldI18n, - defaultFormat, -} from '../src/DecimalFieldI18n'; +import { DecimalFieldI18n, DecimalFieldI18nContextProvider, defaultDecimalFieldI18n } from '../src/DecimalFieldI18n'; +import { formatDecimal } from '../src/formatDecimal'; import { DecimalFieldConfig, defaultPrecision, useDecimalField } from '../src/useDecimalField'; type Config = Omit & { @@ -49,7 +45,7 @@ describe('Decimal field', () => { it('Should format initial value correctly', () => { const [{ result }] = renderUseDecimalField(); - expect(result.current.text).toBe(defaultFormat(0, defaultPrecision)); + expect(result.current.text).toBe(formatDecimal(0, defaultPrecision)); expect(result.current.value).toBe(0); }); From fc47c39f39d378d56c2d525cbd9e4f667fa7ece7 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:23:57 +0300 Subject: [PATCH 53/70] Extracted DateFieldI18nContext --- packages/x/src/DateFieldI18n.tsx | 30 +++++ packages/x/src/formatDate.ts | 9 ++ packages/x/src/useDateField.ts | 71 ++++------- packages/x/tests/useDateField.test.tsx | 157 +++++++++++++------------ 4 files changed, 142 insertions(+), 125 deletions(-) create mode 100644 packages/x/src/DateFieldI18n.tsx create mode 100644 packages/x/src/formatDate.ts diff --git a/packages/x/src/DateFieldI18n.tsx b/packages/x/src/DateFieldI18n.tsx new file mode 100644 index 00000000..c4c41763 --- /dev/null +++ b/packages/x/src/DateFieldI18n.tsx @@ -0,0 +1,30 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +import { formatDate } from './formatDate'; + +export type DateFieldI18n = { + required: string; + invalidInput: string; + minDate: (min: Date, pickTime: boolean) => string; + maxDate: (max: Date, pickTime: boolean) => string; +}; + +export const defaultDateFieldI18n: DateFieldI18n = { + required: 'Field is required', + invalidInput: 'Must be date', + minDate: (min, pickTime) => `Date must not be earlier than ${formatDate(min, pickTime)}`, + maxDate: (max, pickTime) => `Date must not be later than ${formatDate(max, pickTime)}`, +}; + +export const DateFieldI18nContext = createContext(defaultDateFieldI18n); + +export type DateFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const DateFieldI18nContextProvider = ({ i18n, children }: DateFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/x/src/formatDate.ts b/packages/x/src/formatDate.ts new file mode 100644 index 00000000..e33ea1e9 --- /dev/null +++ b/packages/x/src/formatDate.ts @@ -0,0 +1,9 @@ +import dayjs from 'dayjs'; + +export const formatDate = (value: Date | null | undefined, pickTime: boolean) => { + if (!(value instanceof Date)) { + return ''; + } + + return dayjs(value).format(`YYYY-MM-DD${pickTime ? ' HH:mm' : ''}`); +}; diff --git a/packages/x/src/useDateField.ts b/packages/x/src/useDateField.ts index 8c9b9799..dad301bf 100644 --- a/packages/x/src/useDateField.ts +++ b/packages/x/src/useDateField.ts @@ -1,8 +1,10 @@ -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; +import { DateFieldI18nContext } from './DateFieldI18n'; +import { formatDate } from './formatDate'; import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; dayjs.extend(customParseFormat); @@ -17,45 +19,14 @@ const defaultDateTimeFormats = [ 'DD/MM/YYYY HH:mm', ]; -export const defaultLocales: Intl.LocalesArgument = 'EN'; - -export const defaultFormatOptions: Intl.DateTimeFormatOptions = {}; - -const formatDate = ( - value: Date | null | undefined, - locales?: Intl.LocalesArgument, - options?: Intl.DateTimeFormatOptions, -) => { - if (!(value instanceof Date)) { - return ''; - } - - return value.toLocaleString(locales, options); -}; - -export type DateFieldErrorMessages = { - invalidInput: string; - required: string; - earlierThanMinDate: (min: Date, locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string; - laterThanMaxDate: (max: Date, locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string; -}; - -export const defaultErrorMessages: DateFieldErrorMessages = { - invalidInput: 'Must be date', - required: 'Field is required', - earlierThanMinDate: (min, locales, options) => `Date must not be earlier than ${formatDate(min, locales, options)}`, - laterThanMaxDate: (max, locales, options) => `Date must not be later than ${formatDate(max, locales, options)}`, -}; - export type DateFieldConfig = FieldConfig & { required?: boolean; minDate?: Date; maxDate?: Date; pickTime?: boolean; - formatDate?: (date: Date | null | undefined) => string; - parseDate?: (text: string) => Date; - errorMessages?: Partial; + formatDate?: (date: Date | null | undefined, pickTime: boolean) => string; + parseDate?: (text: string, pickTime: boolean) => Date; locales?: Intl.LocalesArgument; formatOptions?: Intl.DateTimeFormatOptions; @@ -70,13 +41,12 @@ export const useDateField = ({ required, minDate, maxDate, - pickTime, + pickTime = false, formatDate: formatDateProps, parseDate: parseDateProps, - errorMessages = defaultErrorMessages, - locales = defaultLocales, - formatOptions = defaultFormatOptions, }: DateFieldConfig): DateFieldBag => { + const i18n = useContext(DateFieldI18nContext); + const parseDate = useCallback( (text: string) => { text = text.trim(); @@ -85,32 +55,35 @@ export const useDateField = ({ return null; } - const errorMessage = errorMessages.invalidInput ?? defaultErrorMessages.invalidInput; - const date = dayjs(text, [...defaultDateFormats, ...(pickTime ? defaultDateTimeFormats : [])], true); if (!date.isValid()) { - throw new ConversionError(errorMessage); + throw new ConversionError(i18n.invalidInput); } return date.toDate(); }, - [errorMessages.invalidInput, pickTime], + [i18n.invalidInput, pickTime], ); const format = useCallback( (value: Date | null | undefined) => { if (formatDateProps) { - return formatDateProps(value); + return formatDateProps(value, pickTime); } - return formatDate(value, locales, formatOptions); + return formatDate(value, pickTime); }, - [formatDateProps, formatOptions, locales], + [formatDateProps, pickTime], + ); + + const parse = useCallback( + (text: string) => (parseDateProps ?? parseDate)(text, pickTime), + [parseDate, parseDateProps, pickTime], ); const dateBag = useConverterField({ - parse: parseDateProps ?? parseDate, + parse, format, name, validator, @@ -121,7 +94,7 @@ export const useDateField = ({ name, validator: (value) => { if (required && !(value instanceof Date)) { - return errorMessages.required ?? defaultErrorMessages.required; + return i18n.required; } if (!(value instanceof Date)) { @@ -129,11 +102,11 @@ export const useDateField = ({ } if (minDate instanceof Date && dayjs(minDate).diff(dayjs(value)) > 0) { - return (errorMessages.earlierThanMinDate ?? defaultErrorMessages.earlierThanMinDate)(minDate); + return i18n.minDate(minDate, pickTime); } if (maxDate instanceof Date && dayjs(value).diff(dayjs(maxDate)) > 0) { - return (errorMessages.laterThanMaxDate ?? defaultErrorMessages.laterThanMaxDate)(maxDate); + return i18n.maxDate(maxDate, pickTime); } return undefined; diff --git a/packages/x/tests/useDateField.test.tsx b/packages/x/tests/useDateField.test.tsx index 7ef80a69..a274882d 100644 --- a/packages/x/tests/useDateField.test.tsx +++ b/packages/x/tests/useDateField.test.tsx @@ -2,20 +2,17 @@ import React from 'react'; import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { - DateFieldConfig, - defaultErrorMessages, - defaultFormatOptions, - defaultLocales, - useDateField, -} from '../src/useDateField'; +import { DateFieldI18n, DateFieldI18nContextProvider, defaultDateFieldI18n } from '../src/DateFieldI18n'; +import { formatDate } from '../src/formatDate'; +import { DateFieldConfig, useDateField } from '../src/useDateField'; type Config = Omit & { initialValue?: Date | null; + i18n?: Partial; }; const renderUseDateField = (config: Config = {}) => { - const { initialValue = null, ...initialProps } = config; + const { initialValue = null, i18n, ...initialProps } = config; const formBag = renderHook(() => useForm({ @@ -33,7 +30,9 @@ const renderUseDateField = (config: Config = {}) => { }), { wrapper: ({ children }) => ( - {children} + + {children} + ), initialProps, }, @@ -43,12 +42,12 @@ const renderUseDateField = (config: Config = {}) => { }; describe('Date field', () => { - it.skip('Should format initial value correctly', () => { + it('Should format initial value correctly', () => { const initialValue = new Date(); const [{ result }] = renderUseDateField({ initialValue }); - expect(result.current.text).toBe(initialValue.toLocaleString(defaultLocales, defaultFormatOptions)); + expect(result.current.text).toBe(formatDate(initialValue, false)); expect(result.current.value?.getTime()).toBe(initialValue.getTime()); }); @@ -60,7 +59,7 @@ describe('Date field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.invalidInput); }); await act(() => { @@ -68,7 +67,7 @@ describe('Date field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.invalidInput); }); await act(() => { @@ -76,7 +75,7 @@ describe('Date field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.invalidInput); + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.invalidInput); }); await act(() => { @@ -115,7 +114,7 @@ describe('Date field', () => { }); }); - it.skip('Should set default error if field is required and empty', async () => { + it('Should set default error if field is required and empty', async () => { const [{ result }] = renderUseDateField({ required: true }); act(() => { @@ -123,11 +122,11 @@ describe('Date field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.required); + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.required); }); }); - it.skip('Should set default error if date is earlier than minDate', async () => { + it('Should set default error if date is earlier than minDate', async () => { const minDate = new Date(2000, 0, 5); const [{ result }] = renderUseDateField({ minDate }); @@ -136,7 +135,7 @@ describe('Date field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.earlierThanMinDate(minDate)); + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.minDate(minDate, false)); }); await act(() => { @@ -148,7 +147,7 @@ describe('Date field', () => { }); }); - it.skip('Should set default error if date is later than maxDate', async () => { + it('Should set default error if date is later than maxDate', async () => { const maxDate = new Date(2000, 0, 5); const [{ result }] = renderUseDateField({ maxDate }); @@ -157,7 +156,7 @@ describe('Date field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultErrorMessages.laterThanMaxDate(maxDate)); + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.maxDate(maxDate, false)); }); await act(() => { @@ -169,9 +168,9 @@ describe('Date field', () => { }); }); - it.skip('Should set custom conversion error correctly', async () => { + it('Should set custom conversion error correctly', async () => { const [{ result }] = renderUseDateField({ - errorMessages: { + i18n: { invalidInput: 'custom', }, }); @@ -201,10 +200,12 @@ describe('Date field', () => { }); }); - it.skip('Should set custom error if field is required and empty', async () => { + it('Should set custom error if field is required and empty', async () => { const [{ result }] = renderUseDateField({ required: true, - errorMessages: { required: 'custom' }, + i18n: { + required: 'custom', + }, }); act(() => { @@ -216,56 +217,60 @@ describe('Date field', () => { }); }); - // it.skip('Should set custom error if field value is less than min', async () => { - // const [{ result }] = renderUseDateField({ - // min: 0.5, - // errorMessages: { lessThanMinValue: () => 'custom' }, - // }); - - // act(() => { - // result.current.control.setValue(0.25); - // }); - - // await waitFor(() => { - // expect(result.current.meta.error?.$error).toBe('custom'); - // }); - // }); - - // it.skip('Should set custom error if field value is more than max', async () => { - // const [{ result }] = renderUseDateField({ - // max: 0.5, - // errorMessages: { moreThanMaxValue: () => 'custom' }, - // }); - - // act(() => { - // result.current.control.setValue(0.75); - // }); - - // await waitFor(() => { - // expect(result.current.meta.error?.$error).toBe('custom'); - // }); - // }); - - // it.skip('Should be able to format decimal differently', () => { - // const formatValue = jest.fn(() => 'custom'); - // const initialValue = 3.14; - // const [{ result }] = renderUseDateField({ formatValue, initialValue }); - - // expect(result.current.text).toBe('custom'); - // expect(formatValue).toBeCalledWith(initialValue); - // }); - - // it.skip('Should call custom parseDecimal function', async () => { - // const parseDecimal = jest.fn(); - - // const [{ result }] = renderUseDateField({ parseDecimal }); - - // await act(() => { - // result.current.onTextChange('0.0'); - // }); - - // await waitFor(() => { - // expect(parseDecimal).toBeCalledWith('0.0'); - // }); - // }); + it('Should set custom error if date is earlier than min date', async () => { + const [{ result }] = renderUseDateField({ + minDate: new Date(42), + i18n: { + minDate: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue(new Date(41)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if date is later than max date', async () => { + const [{ result }] = renderUseDateField({ + maxDate: new Date(42), + i18n: { + maxDate: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue(new Date(43)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should be able to format date differently', () => { + const formatDate = jest.fn(() => 'custom'); + const initialValue = new Date(); + const [{ result }] = renderUseDateField({ formatDate, initialValue }); + + expect(result.current.text).toBe('custom'); + expect(formatDate).toBeCalledWith(initialValue, false); + }); + + it('Should call custom parseDate function', async () => { + const parseDate = jest.fn(); + + const [{ result }] = renderUseDateField({ parseDate }); + + await act(() => { + result.current.onTextChange('0.0'); + }); + + await waitFor(() => { + expect(parseDate).toBeCalledWith('0.0', false); + }); + }); }); From 355d9588f7442f24a9b8032061993613b419702b Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:27:36 +0300 Subject: [PATCH 54/70] Added tests for formatDate --- packages/x/tests/formatDate.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/x/tests/formatDate.test.ts diff --git a/packages/x/tests/formatDate.test.ts b/packages/x/tests/formatDate.test.ts new file mode 100644 index 00000000..29e34ee4 --- /dev/null +++ b/packages/x/tests/formatDate.test.ts @@ -0,0 +1,11 @@ +import { formatDate } from '../src/formatDate'; + +describe('format date', () => { + it('should format correctly without pickTime option', () => { + expect(formatDate(new Date(2023, 8, 12), false)).toBe('2023-09-12'); + }); + + it('should format correctly with pickTime option', () => { + expect(formatDate(new Date(2023, 8, 12, 17, 27, 42, 42), true)).toBe('2023-09-12 17:27'); + }); +}); From 16a0733a19b2fd2f415074475a529baac147b7aa Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:32:50 +0300 Subject: [PATCH 55/70] Added tests for pickTime option --- packages/x/tests/useDateField.test.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/x/tests/useDateField.test.tsx b/packages/x/tests/useDateField.test.tsx index a274882d..28e67075 100644 --- a/packages/x/tests/useDateField.test.tsx +++ b/packages/x/tests/useDateField.test.tsx @@ -266,11 +266,29 @@ describe('Date field', () => { const [{ result }] = renderUseDateField({ parseDate }); await act(() => { - result.current.onTextChange('0.0'); + result.current.onTextChange('2023-09-12'); }); await waitFor(() => { - expect(parseDate).toBeCalledWith('0.0', false); + expect(parseDate).toBeCalledWith('2023-09-12', false); + }); + }); + + it('Should format date with time', () => { + const [{ result }] = renderUseDateField({ pickTime: true, initialValue: new Date(2023, 8, 12, 17, 29) }); + + expect(result.current.text).toBe('2023-09-12 17:29'); + }); + + it('Should parse date with time', async () => { + const [{ result }] = renderUseDateField({ pickTime: true }); + + await act(() => { + result.current.onTextChange('2023-09-12 17:31'); + }); + + await waitFor(() => { + expect(result.current.value?.getTime()).toBe(new Date(2023, 8, 12, 17, 31).getTime()); }); }); }); From b458edc803ab0f85408446e88f359485850063d5 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:36:12 +0300 Subject: [PATCH 56/70] Modified default formatDate function --- packages/x/src/formatDate.ts | 2 +- packages/x/tests/formatDate.test.ts | 4 ++-- packages/x/tests/useDateField.test.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/x/src/formatDate.ts b/packages/x/src/formatDate.ts index e33ea1e9..10e57ef8 100644 --- a/packages/x/src/formatDate.ts +++ b/packages/x/src/formatDate.ts @@ -5,5 +5,5 @@ export const formatDate = (value: Date | null | undefined, pickTime: boolean) => return ''; } - return dayjs(value).format(`YYYY-MM-DD${pickTime ? ' HH:mm' : ''}`); + return dayjs(value).format(`YYYY.MM.DD${pickTime ? ' HH:mm' : ''}`); }; diff --git a/packages/x/tests/formatDate.test.ts b/packages/x/tests/formatDate.test.ts index 29e34ee4..32c7392d 100644 --- a/packages/x/tests/formatDate.test.ts +++ b/packages/x/tests/formatDate.test.ts @@ -2,10 +2,10 @@ import { formatDate } from '../src/formatDate'; describe('format date', () => { it('should format correctly without pickTime option', () => { - expect(formatDate(new Date(2023, 8, 12), false)).toBe('2023-09-12'); + expect(formatDate(new Date(2023, 8, 12), false)).toBe('2023.09.12'); }); it('should format correctly with pickTime option', () => { - expect(formatDate(new Date(2023, 8, 12, 17, 27, 42, 42), true)).toBe('2023-09-12 17:27'); + expect(formatDate(new Date(2023, 8, 12, 17, 27, 42, 42), true)).toBe('2023.09.12 17:27'); }); }); diff --git a/packages/x/tests/useDateField.test.tsx b/packages/x/tests/useDateField.test.tsx index 28e67075..47e5d3e4 100644 --- a/packages/x/tests/useDateField.test.tsx +++ b/packages/x/tests/useDateField.test.tsx @@ -277,7 +277,7 @@ describe('Date field', () => { it('Should format date with time', () => { const [{ result }] = renderUseDateField({ pickTime: true, initialValue: new Date(2023, 8, 12, 17, 29) }); - expect(result.current.text).toBe('2023-09-12 17:29'); + expect(result.current.text).toBe('2023.09.12 17:29'); }); it('Should parse date with time', async () => { From 3d1a94fce78bc3920cc58211478120643af12cbc Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:36:46 +0300 Subject: [PATCH 57/70] Removed unused props --- packages/x/src/useDateField.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/x/src/useDateField.ts b/packages/x/src/useDateField.ts index dad301bf..2dd574d2 100644 --- a/packages/x/src/useDateField.ts +++ b/packages/x/src/useDateField.ts @@ -27,9 +27,6 @@ export type DateFieldConfig = FieldConfig & { formatDate?: (date: Date | null | undefined, pickTime: boolean) => string; parseDate?: (text: string, pickTime: boolean) => Date; - - locales?: Intl.LocalesArgument; - formatOptions?: Intl.DateTimeFormatOptions; }; export type DateFieldBag = ConverterFieldBag; From 1406c5f8a0a61f5417934415d12ddb3154dcb99b Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:42:24 +0300 Subject: [PATCH 58/70] Refactoring --- packages/x/src/useDateField.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/x/src/useDateField.ts b/packages/x/src/useDateField.ts index 2dd574d2..9bbdb8cb 100644 --- a/packages/x/src/useDateField.ts +++ b/packages/x/src/useDateField.ts @@ -39,13 +39,17 @@ export const useDateField = ({ minDate, maxDate, pickTime = false, - formatDate: formatDateProps, - parseDate: parseDateProps, + formatDate: customFormatDate, + parseDate: customParseDate, }: DateFieldConfig): DateFieldBag => { const i18n = useContext(DateFieldI18nContext); - const parseDate = useCallback( + const parse = useCallback( (text: string) => { + if (customParseDate) { + return customParseDate(text, pickTime); + } + text = text.trim(); if (text.length === 0) { @@ -60,23 +64,18 @@ export const useDateField = ({ return date.toDate(); }, - [i18n.invalidInput, pickTime], + [customParseDate, i18n.invalidInput, pickTime], ); const format = useCallback( (value: Date | null | undefined) => { - if (formatDateProps) { - return formatDateProps(value, pickTime); + if (customFormatDate) { + return customFormatDate(value, pickTime); } return formatDate(value, pickTime); }, - [formatDateProps, pickTime], - ); - - const parse = useCallback( - (text: string) => (parseDateProps ?? parseDate)(text, pickTime), - [parseDate, parseDateProps, pickTime], + [customFormatDate, pickTime], ); const dateBag = useConverterField({ From 90e955c60f8f9bb45a748c65cd23fbc884e1cf18 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:45:05 +0300 Subject: [PATCH 59/70] Added changeset --- .changeset/red-flies-study.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-flies-study.md diff --git a/.changeset/red-flies-study.md b/.changeset/red-flies-study.md new file mode 100644 index 00000000..69001b6a --- /dev/null +++ b/.changeset/red-flies-study.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/x': patch +--- + +Created useDateField hook From 04922e1cd292e97a2c19d37055cfdaa6fd5597fb Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 17:53:10 +0300 Subject: [PATCH 60/70] Removed unused type --- packages/x/src/useIntegerField.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index 53b8189d..e4ac7220 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -22,7 +22,7 @@ export type IntegerFieldConfig = FieldConfig & { formatValue?: (value: number | null | undefined) => string; }; -export type IntegerFieldBag = ConverterFieldBag & {}; +export type IntegerFieldBag = ConverterFieldBag; export const useIntegerField = ({ name, From 8bc9f27948035a78a43a3d9a04b0442473578511 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 18:00:11 +0300 Subject: [PATCH 61/70] Extracted `ValueConverter` type --- packages/x/src/formatInteger.ts | 7 ++++++ packages/x/src/useConverterField.ts | 6 +++-- packages/x/src/useIntegerField.ts | 30 ++++++++++------------- packages/x/tests/useIntegerField.test.tsx | 20 ++++++++++++--- 4 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 packages/x/src/formatInteger.ts diff --git a/packages/x/src/formatInteger.ts b/packages/x/src/formatInteger.ts new file mode 100644 index 00000000..49a9a970 --- /dev/null +++ b/packages/x/src/formatInteger.ts @@ -0,0 +1,7 @@ +export const formatInteger = (value: number | null | undefined) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toFixed(0); +}; diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index c0fdc236..b230d679 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -8,10 +8,12 @@ export class ConversionError extends Error { } } -export type ConverterFieldConfig = { +export type ValueConverter = { parse: (value: string) => T; format: (value: T) => string; -} & FieldConfig; +}; + +export type ConverterFieldConfig = ValueConverter & FieldConfig; export type ConverterFieldBag = { text: string; diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index e4ac7220..b2a9b55c 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -1,26 +1,17 @@ import { useCallback, useContext } from 'react'; import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; +import { formatInteger } from './formatInteger'; import { IntegerFieldI18nContext } from './IntegerFieldI18n'; -import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; +import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; const INTEGER_REGEX = /^-?\d+$/; -const formatInteger = (value: number | null | undefined) => { - if (typeof value !== 'number' || !Number.isFinite(value)) { - return ''; - } - - return value.toFixed(0); -}; - export type IntegerFieldConfig = FieldConfig & { required?: boolean; min?: number; max?: number; - - formatValue?: (value: number | null | undefined) => string; -}; +} & Partial>; export type IntegerFieldBag = ConverterFieldBag; @@ -31,12 +22,17 @@ export const useIntegerField = ({ required, min, max, - formatValue = formatInteger, + parse: customParse, + format = formatInteger, }: IntegerFieldConfig): IntegerFieldBag => { const i18n = useContext(IntegerFieldI18nContext); - const parseInteger = useCallback( + const parse = useCallback( (text: string) => { + if (customParse) { + return customParse(text); + } + text = text.trim(); if (text.length === 0) { @@ -55,12 +51,12 @@ export const useIntegerField = ({ return value; }, - [i18n.invalidInput], + [customParse, i18n.invalidInput], ); const integerBag = useConverterField({ - parse: parseInteger, - format: formatValue, + parse, + format, name, validator, schema, diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/x/tests/useIntegerField.test.tsx index 4ee8f569..88767015 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/x/tests/useIntegerField.test.tsx @@ -221,11 +221,25 @@ describe('Integer field', () => { }); it('Should be able to format integer differently', () => { - const formatValue = jest.fn(() => 'custom'); + const format = jest.fn(() => 'custom'); const initialValue = 42; - const [{ result }] = renderUseIntegerField({ formatValue, initialValue }); + const [{ result }] = renderUseIntegerField({ format, initialValue }); expect(result.current.text).toBe('custom'); - expect(formatValue).toBeCalledWith(initialValue); + expect(format).toBeCalledWith(initialValue); + }); + + it('Should call custom parse function', async () => { + const parse = jest.fn(); + + const [{ result }] = renderUseIntegerField({ parse }); + + await act(() => { + result.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(parse).toBeCalledWith('0'); + }); }); }); From dec9992ebf336b3daea17317d65147214655841d Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 18:05:58 +0300 Subject: [PATCH 62/70] Refactoring --- packages/x/src/useConverterField.ts | 6 ++++-- packages/x/src/useDecimalField.ts | 33 +++++++++++++++++------------ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index c0fdc236..b230d679 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -8,10 +8,12 @@ export class ConversionError extends Error { } } -export type ConverterFieldConfig = { +export type ValueConverter = { parse: (value: string) => T; format: (value: T) => string; -} & FieldConfig; +}; + +export type ConverterFieldConfig = ValueConverter & FieldConfig; export type ConverterFieldBag = { text: string; diff --git a/packages/x/src/useDecimalField.ts b/packages/x/src/useDecimalField.ts index 98f42961..35864f41 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/x/src/useDecimalField.ts @@ -3,7 +3,7 @@ import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; import { DecimalFieldI18nContext } from './DecimalFieldI18n'; import { formatDecimal } from './formatDecimal'; -import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; +import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; const DECIMAL_REGEX = /^\d*\.?\d*$/; export const defaultPrecision = 2; @@ -13,11 +13,8 @@ export type DecimalFieldConfig = FieldConfig & { min?: number; max?: number; - format?: (value: number | null | undefined, precision: number) => string; - parse?: (text: string) => number; - precision?: number; -}; +} & Partial>; export type DecimalFieldBag = ConverterFieldBag; @@ -28,16 +25,20 @@ export const useDecimalField = ({ required, min, max, - format, - parse, + format: customFormat, + parse: customParse, precision = defaultPrecision, }: DecimalFieldConfig): DecimalFieldBag => { const i18n = useContext(DecimalFieldI18nContext); - const defaultParse = useCallback( + const parse = useCallback( (text: string) => { text = text.trim(); + if (customParse) { + return customParse(text); + } + if (text.length === 0) { return null; } @@ -54,19 +55,23 @@ export const useDecimalField = ({ return value; }, - [i18n.invalidInput], + [customParse, i18n.invalidInput], ); - const formatValue = useCallback( + const format = useCallback( (value: number | null | undefined) => { - return (format ?? formatDecimal)(value, precision); + if (customFormat) { + return customFormat(value); + } + + return formatDecimal(value, precision); }, - [format, precision], + [customFormat, precision], ); const decimalBag = useConverterField({ - parse: parse ?? defaultParse, - format: formatValue, + parse, + format, name, validator, schema, From 41b1454d616a400dd13096218970a0b0e5dc5080 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 18:06:24 +0300 Subject: [PATCH 63/70] Small fix --- packages/x/src/useIntegerField.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/x/src/useIntegerField.ts b/packages/x/src/useIntegerField.ts index b2a9b55c..8800b467 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/x/src/useIntegerField.ts @@ -29,12 +29,12 @@ export const useIntegerField = ({ const parse = useCallback( (text: string) => { + text = text.trim(); + if (customParse) { return customParse(text); } - text = text.trim(); - if (text.length === 0) { return null; } From fc9c8a749bc4b2d1304e46baa11ef501a6064fcd Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Tue, 12 Sep 2023 18:11:45 +0300 Subject: [PATCH 64/70] Refactoring --- packages/x/src/useConverterField.ts | 6 ++++-- packages/x/src/useDateField.ts | 19 ++++++++----------- packages/x/tests/useDateField.test.tsx | 12 ++++++------ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/x/src/useConverterField.ts b/packages/x/src/useConverterField.ts index c0fdc236..b230d679 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/x/src/useConverterField.ts @@ -8,10 +8,12 @@ export class ConversionError extends Error { } } -export type ConverterFieldConfig = { +export type ValueConverter = { parse: (value: string) => T; format: (value: T) => string; -} & FieldConfig; +}; + +export type ConverterFieldConfig = ValueConverter & FieldConfig; export type ConverterFieldBag = { text: string; diff --git a/packages/x/src/useDateField.ts b/packages/x/src/useDateField.ts index 9bbdb8cb..243e75ba 100644 --- a/packages/x/src/useDateField.ts +++ b/packages/x/src/useDateField.ts @@ -5,7 +5,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat'; import { DateFieldI18nContext } from './DateFieldI18n'; import { formatDate } from './formatDate'; -import { ConversionError, ConverterFieldBag, useConverterField } from './useConverterField'; +import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; dayjs.extend(customParseFormat); @@ -24,10 +24,7 @@ export type DateFieldConfig = FieldConfig & { minDate?: Date; maxDate?: Date; pickTime?: boolean; - - formatDate?: (date: Date | null | undefined, pickTime: boolean) => string; - parseDate?: (text: string, pickTime: boolean) => Date; -}; +} & Partial>; export type DateFieldBag = ConverterFieldBag; @@ -39,19 +36,19 @@ export const useDateField = ({ minDate, maxDate, pickTime = false, - formatDate: customFormatDate, - parseDate: customParseDate, + format: customFormatDate, + parse: customParseDate, }: DateFieldConfig): DateFieldBag => { const i18n = useContext(DateFieldI18nContext); const parse = useCallback( (text: string) => { + text = text.trim(); + if (customParseDate) { - return customParseDate(text, pickTime); + return customParseDate(text); } - text = text.trim(); - if (text.length === 0) { return null; } @@ -70,7 +67,7 @@ export const useDateField = ({ const format = useCallback( (value: Date | null | undefined) => { if (customFormatDate) { - return customFormatDate(value, pickTime); + return customFormatDate(value); } return formatDate(value, pickTime); diff --git a/packages/x/tests/useDateField.test.tsx b/packages/x/tests/useDateField.test.tsx index 47e5d3e4..aa8f7f5f 100644 --- a/packages/x/tests/useDateField.test.tsx +++ b/packages/x/tests/useDateField.test.tsx @@ -252,25 +252,25 @@ describe('Date field', () => { }); it('Should be able to format date differently', () => { - const formatDate = jest.fn(() => 'custom'); + const format = jest.fn(() => 'custom'); const initialValue = new Date(); - const [{ result }] = renderUseDateField({ formatDate, initialValue }); + const [{ result }] = renderUseDateField({ format, initialValue }); expect(result.current.text).toBe('custom'); - expect(formatDate).toBeCalledWith(initialValue, false); + expect(format).toBeCalledWith(initialValue); }); it('Should call custom parseDate function', async () => { - const parseDate = jest.fn(); + const parse = jest.fn(); - const [{ result }] = renderUseDateField({ parseDate }); + const [{ result }] = renderUseDateField({ parse }); await act(() => { result.current.onTextChange('2023-09-12'); }); await waitFor(() => { - expect(parseDate).toBeCalledWith('2023-09-12', false); + expect(parse).toBeCalledWith('2023-09-12'); }); }); From 006c31fa31a51f1e470e42fcf732eb5ea9368d94 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 13 Sep 2023 16:09:24 +0300 Subject: [PATCH 65/70] Configured tsc build in core package --- packages/core/aqu.config.json | 12 ------- packages/core/package.json | 5 ++- packages/core/src/components/ReactiveForm.tsx | 5 ++- packages/core/src/hooks/useFieldValueArray.ts | 11 ++++--- packages/core/src/hooks/useForm.ts | 28 ++++++++++------ packages/core/src/utils/deepRemoveEmpty.ts | 7 ++-- packages/core/src/utils/excludeOverlaps.ts | 6 +++- packages/core/src/utils/flattenObject.ts | 6 ++-- packages/core/src/utils/getDifferenceMap.ts | 2 +- packages/core/src/utils/overrideMerge.ts | 4 +-- packages/core/src/utils/setNestedValues.ts | 9 +++--- packages/core/tsconfig.json | 32 +++++++++---------- pnpm-lock.yaml | 13 +++----- 13 files changed, 72 insertions(+), 68 deletions(-) delete mode 100644 packages/core/aqu.config.json diff --git a/packages/core/aqu.config.json b/packages/core/aqu.config.json deleted file mode 100644 index cd4658b7..00000000 --- a/packages/core/aqu.config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "buildOptions": { - "target": ["es2019", "chrome58", "firefox57", "safari11", "edge18", "node12"], - "preserveSymlinks": false - }, - "dtsBundleGeneratorOptions": { - "libraries": { - "importedLibraries": ["stocked", "yup", "react", "pxth"], - "allowedTypesLibraries": [] - } - } -} diff --git a/packages/core/package.json b/packages/core/package.json index f67d1987..40f4c888 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,8 +15,8 @@ "directory": "prepared-package" }, "scripts": { - "build": "aqu build && rimraf ./prepared-package && clean-publish", - "dev": "aqu watch --no-cleanup", + "build": "tsc && rimraf ./prepared-package && clean-publish", + "dev": "tsc --watch", "lint": "eslint .", "lint:fix": "npm run lint --fix", "test": "jest --passWithNoTests", @@ -38,7 +38,6 @@ "@types/lodash": "4.14.161", "@types/node": "^18.11.18", "@types/react": "18.0.23", - "aqu": "0.4.3", "jest": "29.2.2", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/core/src/components/ReactiveForm.tsx b/packages/core/src/components/ReactiveForm.tsx index 222121f4..06c796e4 100644 --- a/packages/core/src/components/ReactiveForm.tsx +++ b/packages/core/src/components/ReactiveForm.tsx @@ -16,7 +16,10 @@ export type ReactiveFormProps = PropsWithChildren * ``` */ -export const ReactiveForm = ({ children, ...config }: ReactiveFormProps) => { +export const ReactiveForm = >({ + children, + ...config +}: ReactiveFormProps) => { const formBag = useForm(config); return {children}; diff --git a/packages/core/src/hooks/useFieldValueArray.ts b/packages/core/src/hooks/useFieldValueArray.ts index a2850ea6..6b3974c6 100644 --- a/packages/core/src/hooks/useFieldValueArray.ts +++ b/packages/core/src/hooks/useFieldValueArray.ts @@ -13,12 +13,13 @@ export const useFieldValueArray = (paths: FieldValueArrayConfi getFieldValue, } = useFormContext(); - const [object, setObject] = useState(() => - Object.entries(paths).reduce((acc, [to, from]) => { - acc[to] = getFieldValue(from as Pxth); + const [object, setObject] = useState( + () => + Object.entries(paths).reduce((acc, [to, from]) => { + acc[to] = getFieldValue(from as Pxth); - return acc; - }, {} as T), + return acc; + }, {} as Record) as T, ); useEffect(() => { diff --git a/packages/core/src/hooks/useForm.ts b/packages/core/src/hooks/useForm.ts index 114ae142..a3b8ab8e 100644 --- a/packages/core/src/hooks/useForm.ts +++ b/packages/core/src/hooks/useForm.ts @@ -77,7 +77,9 @@ const deepCustomizer = (src1: unknown, src2: unknown) => { const formMetaPaths = createPxth([]); -export const useForm = (initialConfig: FormConfig): FormShared => { +export const useForm = >( + initialConfig: FormConfig, +): FormShared => { const config = usePluginConfigDecorators(initialConfig); const { schema, disablePureFieldsValidation } = config; @@ -126,12 +128,19 @@ export const useForm = (initialConfig: FormConfig }, []); const normalizeErrors = useCallback( - (errors: FieldError, source: object, compare: object): FieldError => { + ( + errors: FieldError, + source: Record, + compare: Record, + ): FieldError => { if (!disablePureFieldsValidation) { return errors; } - return merge(setNestedValues(errors as object, undefined), excludeOverlaps(source, compare, errors)); + return merge( + setNestedValues(errors as Record, undefined), + excludeOverlaps(source, compare, errors), + ); }, [disablePureFieldsValidation], ); @@ -147,8 +156,8 @@ export const useForm = (initialConfig: FormConfig return normalizeErrors( valueErrors, - deepGet(allValues, name) as object, - deepGet(initialValuesRef.current, name) as object, + deepGet(allValues, name) as Record, + deepGet(initialValuesRef.current, name) as Record, ); } @@ -282,22 +291,23 @@ export const useForm = (initialConfig: FormConfig ); const updateFormValidness = useCallback( - ({ values }: BatchUpdate) => setFormMeta(formMetaPaths.isValid, deepRemoveEmpty(values) === undefined), + ({ values }: BatchUpdate>) => + setFormMeta(formMetaPaths.isValid, deepRemoveEmpty(values) === undefined), [setFormMeta], ); const validateUpdatedFields = useCallback( - async ({ values, origin }: BatchUpdate) => { + async ({ values, origin }: BatchUpdate>) => { const { attachPath, errors } = await validateBranch(origin, values); const onlyNecessaryErrors = deepGet(errors, attachPath) as FieldError; const normalizedErrors = normalizeErrors( onlyNecessaryErrors, values, - deepGet(initialValuesRef.current, attachPath) as object, + deepGet(initialValuesRef.current, attachPath) as Record, ); - setFieldError(attachPath, (old) => overrideMerge(old ?? {}, normalizedErrors as object)); + setFieldError(attachPath, (old) => overrideMerge(old ?? {}, normalizedErrors as Record)); }, [normalizeErrors, setFieldError, validateBranch], ); diff --git a/packages/core/src/utils/deepRemoveEmpty.ts b/packages/core/src/utils/deepRemoveEmpty.ts index 52ed2066..68904415 100644 --- a/packages/core/src/utils/deepRemoveEmpty.ts +++ b/packages/core/src/utils/deepRemoveEmpty.ts @@ -1,7 +1,7 @@ import isEmpty from 'lodash/isEmpty'; import isNil from 'lodash/isNil'; -export const deepRemoveEmpty = (obj: object): object | undefined => { +export const deepRemoveEmpty = (obj: Record): object | undefined => { if (Array.isArray(obj)) { const newArr = Object.assign( [], @@ -15,14 +15,15 @@ export const deepRemoveEmpty = (obj: object): object | undefined => { return Object.values(newArr).every(isNil) ? undefined : newArr; } else if (obj !== null && typeof obj === 'object') { const newObj = Object.keys(obj).reduce((acc, key) => { - const value = typeof obj[key] === 'object' ? deepRemoveEmpty(obj[key]) : obj[key]; + const value = + typeof obj[key] === 'object' ? deepRemoveEmpty(obj[key] as Record) : obj[key]; if (!isNil(value)) { acc[key] = value; } return acc; - }, {}); + }, {} as Record); return isEmpty(newObj) ? undefined : newObj; } return undefined; diff --git a/packages/core/src/utils/excludeOverlaps.ts b/packages/core/src/utils/excludeOverlaps.ts index 8266a91d..7c02cd5b 100644 --- a/packages/core/src/utils/excludeOverlaps.ts +++ b/packages/core/src/utils/excludeOverlaps.ts @@ -3,7 +3,11 @@ import { RootPathToken } from 'pxth'; import { getDifferenceMap } from './getDifferenceMap'; -export const excludeOverlaps = (source: object, compare: object, exclusionObject: T): Partial => { +export const excludeOverlaps = ( + source: Record, + compare: Record, + exclusionObject: T, +): Partial => { const diffMap = getDifferenceMap(source, compare); if (diffMap[RootPathToken]) return {}; diff --git a/packages/core/src/utils/flattenObject.ts b/packages/core/src/utils/flattenObject.ts index f46076e6..9e7186f8 100644 --- a/packages/core/src/utils/flattenObject.ts +++ b/packages/core/src/utils/flattenObject.ts @@ -2,8 +2,8 @@ import { RootPath, RootPathToken } from 'pxth'; import { joinPaths } from './joinPaths'; -export const flattenObject = (obj: object): Record => { - const queue: Array<[string | RootPath, object]> = [[RootPathToken, obj]]; +export const flattenObject = (obj: Record): Record => { + const queue: Array<[string | RootPath, Record]> = [[RootPathToken, obj]]; const result: Record = {}; @@ -16,7 +16,7 @@ export const flattenObject = (obj: object): Record => { if (typeof item !== 'object' || item === null || Object.keys(item).length === 0) { result[pathToItem] = item; } else { - queue.push([pathToItem, item]); + queue.push([pathToItem, item as Record]); } } } diff --git a/packages/core/src/utils/getDifferenceMap.ts b/packages/core/src/utils/getDifferenceMap.ts index 3ef1ce78..c56602ea 100644 --- a/packages/core/src/utils/getDifferenceMap.ts +++ b/packages/core/src/utils/getDifferenceMap.ts @@ -24,7 +24,7 @@ const isInnerPath = (parent: string | RootPath, child: string) => { return child.indexOf(parent + '.') === 0; }; -export const getDifferenceMap = (obj1: object, obj2: object): DifferenceMap => { +export const getDifferenceMap = (obj1: Record, obj2: Record): DifferenceMap => { const flattenedObj1 = flattenObject(obj1); const flattenedObj2 = flattenObject(obj2); diff --git a/packages/core/src/utils/overrideMerge.ts b/packages/core/src/utils/overrideMerge.ts index f1b527be..a6464a62 100644 --- a/packages/core/src/utils/overrideMerge.ts +++ b/packages/core/src/utils/overrideMerge.ts @@ -1,7 +1,7 @@ import cloneDeep from 'lodash/cloneDeep'; import isObject from 'lodash/isObject'; -export const overrideMerge = (object: object, source: object) => { +export const overrideMerge = (object: Record, source: Record) => { object = cloneDeep(object); const queue = [[object, source]]; @@ -19,7 +19,7 @@ export const overrideMerge = (object: object, source: object) => { const compareSourceValue = currentSource[key]; if (isObject(compareValue) && isObject(compareSourceValue)) { - queue.push([compareValue, compareSourceValue]); + queue.push([compareValue as Record, compareSourceValue as Record]); } else { currentObject[key] = cloneDeep(currentSource[key]); } diff --git a/packages/core/src/utils/setNestedValues.ts b/packages/core/src/utils/setNestedValues.ts index 91e4736d..c412c67e 100644 --- a/packages/core/src/utils/setNestedValues.ts +++ b/packages/core/src/utils/setNestedValues.ts @@ -2,13 +2,13 @@ import isObject from 'lodash/isObject'; import { NestedObject } from '../typings/NestedObject'; -export const setNestedValues = ( +export const setNestedValues = , Value>( exampleObject: Example, value: Value, visited: WeakMap = new WeakMap(), output: NestedObject = {} as NestedObject, ): NestedObject => - Object.keys(exampleObject).reduce>((acc, key) => { + Object.keys(exampleObject).reduce((acc, key) => { const part = exampleObject[key]; if (isObject(part)) { @@ -16,11 +16,12 @@ export const setNestedValues = ( visited.set(part, true); acc[key] = Array.isArray(part) ? [] : {}; acc[key] = Object.assign(acc[key], value); - setNestedValues(part, value, visited, acc[key]); + setNestedValues(part as Record, value, visited, acc[key]); } } else { acc[key] = value; } return acc; - }, output); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }, output as Record) as NestedObject; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 298bf57c..d3e9461a 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,24 +1,24 @@ { "compilerOptions": { - "outDir": "dist", - "module": "esnext", - "lib": ["dom", "esnext"], - "moduleResolution": "node", - "jsx": "react", - "sourceMap": true, + "target": "es6", + "jsx": "react-jsx", + "module": "CommonJS", "declaration": true, + "strict": true, "esModuleInterop": true, - "noImplicitReturns": false, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "lib": ["es2022", "DOM"], + "noLib": false, + "rootDir": ".", + "skipLibCheck": true, "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "allowJs": true, + "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "strict": true, - "importHelpers": true + "resolveJsonModule": true, + "isolatedModules": true, + "outDir": "dist" }, - "include": ["src", "tests"] + "include": ["src"], + "exclude": ["node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46f2d6af..9c63c0ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 8.1.0(size-limit@8.1.0) aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) clean-publish: specifier: 4.2.0 version: 4.2.0 @@ -68,7 +68,7 @@ importers: version: 18.0.23 aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) jest: specifier: 29.2.2 version: 29.2.2(@types/node@18.11.18) @@ -125,9 +125,6 @@ importers: '@types/react': specifier: 18.0.23 version: 18.0.23 - aqu: - specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) jest: specifier: 29.2.2 version: 29.2.2(@types/node@18.11.18) @@ -189,7 +186,7 @@ importers: version: 18.0.6 aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) cpy-cli: specifier: ^5.0.0 version: 5.0.0 @@ -251,7 +248,7 @@ importers: version: 18.0.23 aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) jest: specifier: 29.2.2 version: 29.2.2(@types/node@18.11.18) @@ -3016,7 +3013,7 @@ packages: picomatch: 2.3.1 dev: true - /aqu@0.4.3(@babel/core@7.19.6)(@types/node@18.11.18): + /aqu@0.4.3(@babel/core@7.19.6): resolution: {integrity: sha512-3Jq4rKSL0lRqkMnUtjbFMliGPpCeiOTmWtpY0ryFH1M10igx6Go+TOoa+StADJjgiwgMg6hYUdam+YcnBsn7kQ==} engines: {node: ^10.12.0 || >=12.0.0} hasBin: true From 0d9ec1019c9f0c055d488065741a0566e367f795 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 13 Sep 2023 16:18:35 +0300 Subject: [PATCH 66/70] Fixed ts errors in tests --- packages/core/src/utils/deepRemoveEmpty.ts | 5 +++-- packages/core/tests/components/FormPlugins.test.tsx | 4 ++-- packages/core/tests/hooks/useArrayControl.test.tsx | 2 +- packages/core/tests/hooks/useArrayField.test.tsx | 2 +- packages/core/tests/hooks/useField.test.tsx | 2 +- packages/core/tests/hooks/useFieldError.test.tsx | 2 +- packages/core/tests/hooks/useFieldTouched.test.tsx | 2 +- packages/core/tests/hooks/useFieldValidator.test.tsx | 2 +- packages/core/tests/hooks/useFieldValue.test.tsx | 2 +- packages/core/tests/hooks/useFieldValueArray.test.tsx | 2 +- packages/core/tests/hooks/useFormControl.proxy.test.tsx | 2 +- packages/core/tests/utils/deepRemoveEmpty.test.ts | 2 +- packages/core/tsconfig.json | 2 +- 13 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/core/src/utils/deepRemoveEmpty.ts b/packages/core/src/utils/deepRemoveEmpty.ts index 68904415..15e5146b 100644 --- a/packages/core/src/utils/deepRemoveEmpty.ts +++ b/packages/core/src/utils/deepRemoveEmpty.ts @@ -1,7 +1,7 @@ import isEmpty from 'lodash/isEmpty'; import isNil from 'lodash/isNil'; -export const deepRemoveEmpty = (obj: Record): object | undefined => { +export const deepRemoveEmpty = (obj: object): object | undefined => { if (Array.isArray(obj)) { const newArr = Object.assign( [], @@ -14,9 +14,10 @@ export const deepRemoveEmpty = (obj: Record): object | undefine ); return Object.values(newArr).every(isNil) ? undefined : newArr; } else if (obj !== null && typeof obj === 'object') { + const casted = obj as Record; const newObj = Object.keys(obj).reduce((acc, key) => { const value = - typeof obj[key] === 'object' ? deepRemoveEmpty(obj[key] as Record) : obj[key]; + typeof casted[key] === 'object' ? deepRemoveEmpty(casted[key] as Record) : casted[key]; if (!isNil(value)) { acc[key] = value; diff --git a/packages/core/tests/components/FormPlugins.test.tsx b/packages/core/tests/components/FormPlugins.test.tsx index efaf2e44..a3a14f98 100644 --- a/packages/core/tests/components/FormPlugins.test.tsx +++ b/packages/core/tests/components/FormPlugins.test.tsx @@ -12,13 +12,13 @@ import { useFormContext, } from '../../src'; -const renderPlugins = (config: FormConfig, plugins: PluginArray) => { +const renderPlugins = >(config: FormConfig, plugins: PluginArray) => { return renderHook(() => useForm(config), { wrapper: ({ children }) => {children}, }); }; -const renderForm = (config: FormConfig, plugins: PluginArray) => { +const renderForm = >(config: FormConfig, plugins: PluginArray) => { const { result: { current: bag }, } = renderPlugins(config, plugins); diff --git a/packages/core/tests/hooks/useArrayControl.test.tsx b/packages/core/tests/hooks/useArrayControl.test.tsx index 4bd884d7..29cbb943 100644 --- a/packages/core/tests/hooks/useArrayControl.test.tsx +++ b/packages/core/tests/hooks/useArrayControl.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { ArrayControl, FormConfig, FormShared, ReactiveFormProvider, useArrayControl, useForm } from '../../src'; -const renderArrayControl = ( +const renderArrayControl = , V>( name: Pxth, config: FormConfig, ): [RenderHookResult, undefined>, FormShared] => { diff --git a/packages/core/tests/hooks/useArrayField.test.tsx b/packages/core/tests/hooks/useArrayField.test.tsx index f01e4e34..9b3d5b0a 100644 --- a/packages/core/tests/hooks/useArrayField.test.tsx +++ b/packages/core/tests/hooks/useArrayField.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { ArrayFieldProps, FormConfig, FormShared, ReactiveFormProvider, useArrayField, useForm } from '../../src'; -const renderArrayField = ( +const renderArrayField = >( name: Pxth, config: FormConfig, ): RenderHookResult, undefined> => { diff --git a/packages/core/tests/hooks/useField.test.tsx b/packages/core/tests/hooks/useField.test.tsx index 0890439f..f7b900ba 100644 --- a/packages/core/tests/hooks/useField.test.tsx +++ b/packages/core/tests/hooks/useField.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FieldContext, FormConfig, FormShared, ReactiveFormProvider, useField, useForm } from '../../src'; -const renderField = ( +const renderField = >( name: Pxth, config: FormConfig, ): RenderHookResult, undefined> => { diff --git a/packages/core/tests/hooks/useFieldError.test.tsx b/packages/core/tests/hooks/useFieldError.test.tsx index d35277ab..84f98725 100644 --- a/packages/core/tests/hooks/useFieldError.test.tsx +++ b/packages/core/tests/hooks/useFieldError.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FieldError, FormConfig, FormShared, ReactiveFormProvider, useFieldError, useForm } from '../../src'; -const renderFieldError = ( +const renderFieldError = >( name: Pxth, config: FormConfig, ): RenderHookResult<[FieldError | undefined, Dispatch>], undefined> => { diff --git a/packages/core/tests/hooks/useFieldTouched.test.tsx b/packages/core/tests/hooks/useFieldTouched.test.tsx index fc372ed3..9f9cc1f0 100644 --- a/packages/core/tests/hooks/useFieldTouched.test.tsx +++ b/packages/core/tests/hooks/useFieldTouched.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FieldTouched, FormConfig, FormShared, ReactiveFormProvider, useFieldTouched, useForm } from '../../src'; -const renderFieldTouched = ( +const renderFieldTouched = >( name: Pxth, config: FormConfig, ): RenderHookResult<[FieldTouched | undefined, Dispatch>], undefined> => { diff --git a/packages/core/tests/hooks/useFieldValidator.test.tsx b/packages/core/tests/hooks/useFieldValidator.test.tsx index 22587bf2..6e956d8a 100644 --- a/packages/core/tests/hooks/useFieldValidator.test.tsx +++ b/packages/core/tests/hooks/useFieldValidator.test.tsx @@ -12,7 +12,7 @@ import { useForm, } from '../../src'; -const renderUseFieldValidator = ( +const renderUseFieldValidator = >( config: UseFieldValidatorConfig, formConfig: FormConfig, ) => { diff --git a/packages/core/tests/hooks/useFieldValue.test.tsx b/packages/core/tests/hooks/useFieldValue.test.tsx index 548f678f..019c6001 100644 --- a/packages/core/tests/hooks/useFieldValue.test.tsx +++ b/packages/core/tests/hooks/useFieldValue.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FormConfig, FormShared, ReactiveFormProvider, useFieldValue, useForm } from '../../src'; -const renderFieldValue = ( +const renderFieldValue = >( name: Pxth, config: FormConfig, ): RenderHookResult<[V, Dispatch], undefined> => { diff --git a/packages/core/tests/hooks/useFieldValueArray.test.tsx b/packages/core/tests/hooks/useFieldValueArray.test.tsx index c3504b81..468c1020 100644 --- a/packages/core/tests/hooks/useFieldValueArray.test.tsx +++ b/packages/core/tests/hooks/useFieldValueArray.test.tsx @@ -11,7 +11,7 @@ import { useForm, } from '../../src'; -const renderFieldValueArray = ( +const renderFieldValueArray = , T extends Record>( paths: FieldValueArrayConfig, config: FormConfig, ): [RenderHookResult, FormShared] => { diff --git a/packages/core/tests/hooks/useFormControl.proxy.test.tsx b/packages/core/tests/hooks/useFormControl.proxy.test.tsx index 303362ad..bd8e2d70 100644 --- a/packages/core/tests/hooks/useFormControl.proxy.test.tsx +++ b/packages/core/tests/hooks/useFormControl.proxy.test.tsx @@ -6,7 +6,7 @@ import { MappingProxy, StockProxy } from 'stocked'; import { FormProxyProvider, FormShared, ReactiveFormProvider, useForm, useFormContext } from '../../src'; import { FormControlConfig } from '../../src/hooks/useFormControl'; -const renderFormContextWithProxy = ( +const renderFormContextWithProxy = >( config: FormControlConfig, proxy: StockProxy, ): [RenderHookResult, undefined>, FormShared] => { diff --git a/packages/core/tests/utils/deepRemoveEmpty.test.ts b/packages/core/tests/utils/deepRemoveEmpty.test.ts index 5bd8e825..00395b20 100644 --- a/packages/core/tests/utils/deepRemoveEmpty.test.ts +++ b/packages/core/tests/utils/deepRemoveEmpty.test.ts @@ -17,7 +17,7 @@ describe('deepRemoveEmpty', () => { }); it('should handle nulls', () => { - expect(deepRemoveEmpty(null)).toBe(undefined); + expect(deepRemoveEmpty(null as unknown as object)).toBe(undefined); }); it('should shake objects', () => { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d3e9461a..46b5d03c 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -19,6 +19,6 @@ "isolatedModules": true, "outDir": "dist" }, - "include": ["src"], + "include": ["src", "tests"], "exclude": ["node_modules"] } From a7e729eee893f58e517b75d0c52a00a37811f822 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 13 Sep 2023 16:29:32 +0300 Subject: [PATCH 67/70] Moved x package in core --- .changeset/chilly-clocks-train.md | 2 +- .changeset/healthy-comics-refuse.md | 2 +- .changeset/red-badgers-doubt.md | 2 +- .changeset/red-flies-study.md | 2 +- .changeset/stale-cars-heal.md | 2 +- packages/core/package.json | 1 + .../src/hooks}/BooleanFieldI18n.tsx | 0 .../src => core/src/hooks}/DateFieldI18n.tsx | 2 +- .../src/hooks}/DecimalFieldI18n.tsx | 2 +- .../src/hooks}/IntegerFieldI18n.tsx | 0 .../src/hooks}/StringFieldI18n.tsx | 0 .../src => core/src/hooks}/useBooleanField.ts | 4 +- .../src/hooks}/useConverterField.ts | 7 ++- .../{x/src => core/src/hooks}/useDateField.ts | 5 +- .../src => core/src/hooks}/useDecimalField.ts | 5 +- .../src => core/src/hooks}/useIntegerField.ts | 5 +- .../src => core/src/hooks}/useStringField.ts | 4 +- packages/core/src/index.ts | 17 ++++++ .../{x/src => core/src/utils}/formatDate.ts | 0 .../src => core/src/utils}/formatDecimal.ts | 0 .../src => core/src/utils}/formatInteger.ts | 0 .../tests/hooks}/useBooleanField.test.tsx | 12 +++- .../tests/hooks}/useConverterField.test.tsx | 3 +- .../tests/hooks}/useDateField.test.tsx | 14 +++-- .../tests/hooks}/useDecimalField.test.tsx | 15 +++-- .../tests/hooks}/useIntegerField.test.tsx | 12 +++- .../tests/hooks}/useStringField.test.tsx | 12 +++- .../tests/utils}/formatDate.test.ts | 2 +- packages/core/tsconfig.json | 2 +- packages/x/.eslintignore | 3 - packages/x/.eslintrc.json | 3 - packages/x/CHANGELOG.md | 56 ------------------- packages/x/LICENSE | 21 ------- packages/x/README.md | 11 ---- packages/x/aqu.config.json | 12 ---- packages/x/jest.config.js | 24 -------- packages/x/package.json | 54 ------------------ packages/x/src/index.ts | 2 - packages/x/src/plugin.ts | 7 --- packages/x/tsconfig.json | 25 --------- packages/x/tsconfig.test.json | 15 ----- pnpm-lock.yaml | 3 + 42 files changed, 100 insertions(+), 270 deletions(-) rename packages/{x/src => core/src/hooks}/BooleanFieldI18n.tsx (100%) rename packages/{x/src => core/src/hooks}/DateFieldI18n.tsx (95%) rename packages/{x/src => core/src/hooks}/DecimalFieldI18n.tsx (95%) rename packages/{x/src => core/src/hooks}/IntegerFieldI18n.tsx (100%) rename packages/{x/src => core/src/hooks}/StringFieldI18n.tsx (100%) rename packages/{x/src => core/src/hooks}/useBooleanField.ts (82%) rename packages/{x/src => core/src/hooks}/useConverterField.ts (91%) rename packages/{x/src => core/src/hooks}/useDateField.ts (94%) rename packages/{x/src => core/src/hooks}/useDecimalField.ts (93%) rename packages/{x/src => core/src/hooks}/useIntegerField.ts (91%) rename packages/{x/src => core/src/hooks}/useStringField.ts (88%) rename packages/{x/src => core/src/utils}/formatDate.ts (100%) rename packages/{x/src => core/src/utils}/formatDecimal.ts (100%) rename packages/{x/src => core/src/utils}/formatInteger.ts (100%) rename packages/{x/tests => core/tests/hooks}/useBooleanField.test.tsx (91%) rename packages/{x/tests => core/tests/hooks}/useConverterField.test.tsx (97%) rename packages/{x/tests => core/tests/hooks}/useDateField.test.tsx (95%) rename packages/{x/tests => core/tests/hooks}/useDecimalField.test.tsx (95%) rename packages/{x/tests => core/tests/hooks}/useIntegerField.test.tsx (95%) rename packages/{x/tests => core/tests/hooks}/useStringField.test.tsx (96%) rename packages/{x/tests => core/tests/utils}/formatDate.test.ts (85%) delete mode 100644 packages/x/.eslintignore delete mode 100644 packages/x/.eslintrc.json delete mode 100644 packages/x/CHANGELOG.md delete mode 100644 packages/x/LICENSE delete mode 100644 packages/x/README.md delete mode 100644 packages/x/aqu.config.json delete mode 100644 packages/x/jest.config.js delete mode 100644 packages/x/package.json delete mode 100644 packages/x/src/index.ts delete mode 100644 packages/x/src/plugin.ts delete mode 100644 packages/x/tsconfig.json delete mode 100644 packages/x/tsconfig.test.json diff --git a/.changeset/chilly-clocks-train.md b/.changeset/chilly-clocks-train.md index b6950e3f..80ff5aed 100644 --- a/.changeset/chilly-clocks-train.md +++ b/.changeset/chilly-clocks-train.md @@ -1,5 +1,5 @@ --- -'@reactive-forms/x': patch +'@reactive-forms/core': patch --- Created useStringField hook diff --git a/.changeset/healthy-comics-refuse.md b/.changeset/healthy-comics-refuse.md index 41f993ad..b99cfadb 100644 --- a/.changeset/healthy-comics-refuse.md +++ b/.changeset/healthy-comics-refuse.md @@ -1,5 +1,5 @@ --- -'@reactive-forms/x': minor +'@reactive-forms/core': minor --- Created useDecimalField hook diff --git a/.changeset/red-badgers-doubt.md b/.changeset/red-badgers-doubt.md index 36d892b7..c1a17c52 100644 --- a/.changeset/red-badgers-doubt.md +++ b/.changeset/red-badgers-doubt.md @@ -1,5 +1,5 @@ --- -'@reactive-forms/x': minor +'@reactive-forms/core': minor --- Created useIntegerField hook diff --git a/.changeset/red-flies-study.md b/.changeset/red-flies-study.md index 69001b6a..a070de20 100644 --- a/.changeset/red-flies-study.md +++ b/.changeset/red-flies-study.md @@ -1,5 +1,5 @@ --- -'@reactive-forms/x': patch +'@reactive-forms/core': patch --- Created useDateField hook diff --git a/.changeset/stale-cars-heal.md b/.changeset/stale-cars-heal.md index fea90e02..e89f101e 100644 --- a/.changeset/stale-cars-heal.md +++ b/.changeset/stale-cars-heal.md @@ -1,5 +1,5 @@ --- -'@reactive-forms/x': minor +'@reactive-forms/core': minor --- Created useBooleanField hook diff --git a/packages/core/package.json b/packages/core/package.json index 40f4c888..460c5c1a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,6 +24,7 @@ "test:watch": "jest --passWithNoTests --watch" }, "dependencies": { + "dayjs": "^1.11.9", "lodash": "4.17.21", "lodash-es": "4.17.15", "pxth": "0.7.0", diff --git a/packages/x/src/BooleanFieldI18n.tsx b/packages/core/src/hooks/BooleanFieldI18n.tsx similarity index 100% rename from packages/x/src/BooleanFieldI18n.tsx rename to packages/core/src/hooks/BooleanFieldI18n.tsx diff --git a/packages/x/src/DateFieldI18n.tsx b/packages/core/src/hooks/DateFieldI18n.tsx similarity index 95% rename from packages/x/src/DateFieldI18n.tsx rename to packages/core/src/hooks/DateFieldI18n.tsx index c4c41763..07e31ef6 100644 --- a/packages/x/src/DateFieldI18n.tsx +++ b/packages/core/src/hooks/DateFieldI18n.tsx @@ -1,7 +1,7 @@ import React, { createContext, PropsWithChildren } from 'react'; import merge from 'lodash/merge'; -import { formatDate } from './formatDate'; +import { formatDate } from '../utils/formatDate'; export type DateFieldI18n = { required: string; diff --git a/packages/x/src/DecimalFieldI18n.tsx b/packages/core/src/hooks/DecimalFieldI18n.tsx similarity index 95% rename from packages/x/src/DecimalFieldI18n.tsx rename to packages/core/src/hooks/DecimalFieldI18n.tsx index 1fae42f0..58d3b0b6 100644 --- a/packages/x/src/DecimalFieldI18n.tsx +++ b/packages/core/src/hooks/DecimalFieldI18n.tsx @@ -1,7 +1,7 @@ import React, { createContext, PropsWithChildren } from 'react'; import merge from 'lodash/merge'; -import { formatDecimal } from './formatDecimal'; +import { formatDecimal } from '../utils/formatDecimal'; export type DecimalFieldI18n = { required: string; diff --git a/packages/x/src/IntegerFieldI18n.tsx b/packages/core/src/hooks/IntegerFieldI18n.tsx similarity index 100% rename from packages/x/src/IntegerFieldI18n.tsx rename to packages/core/src/hooks/IntegerFieldI18n.tsx diff --git a/packages/x/src/StringFieldI18n.tsx b/packages/core/src/hooks/StringFieldI18n.tsx similarity index 100% rename from packages/x/src/StringFieldI18n.tsx rename to packages/core/src/hooks/StringFieldI18n.tsx diff --git a/packages/x/src/useBooleanField.ts b/packages/core/src/hooks/useBooleanField.ts similarity index 82% rename from packages/x/src/useBooleanField.ts rename to packages/core/src/hooks/useBooleanField.ts index 52575386..b7df1359 100644 --- a/packages/x/src/useBooleanField.ts +++ b/packages/core/src/hooks/useBooleanField.ts @@ -1,7 +1,9 @@ import { useContext } from 'react'; -import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; import { BooleanFieldI18nContext } from './BooleanFieldI18n'; +import { FieldConfig, useField } from './useField'; +import { useFieldValidator } from './useFieldValidator'; +import { FieldContext } from '../typings/FieldContext'; export type BooleanFieldConfig = FieldConfig & { required?: boolean; diff --git a/packages/x/src/useConverterField.ts b/packages/core/src/hooks/useConverterField.ts similarity index 91% rename from packages/x/src/useConverterField.ts rename to packages/core/src/hooks/useConverterField.ts index b230d679..a05e723c 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/core/src/hooks/useConverterField.ts @@ -1,7 +1,12 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { FieldConfig, FieldContext, FieldError, FieldTouched, useField, useFieldValidator } from '@reactive-forms/core'; import isObject from 'lodash/isObject'; +import { FieldConfig, useField } from './useField'; +import { useFieldValidator } from './useFieldValidator'; +import { FieldContext } from '../typings/FieldContext'; +import { FieldError } from '../typings/FieldError'; +import { FieldTouched } from '../typings/FieldTouched'; + export class ConversionError extends Error { public constructor(errorMessage: string) { super(errorMessage); diff --git a/packages/x/src/useDateField.ts b/packages/core/src/hooks/useDateField.ts similarity index 94% rename from packages/x/src/useDateField.ts rename to packages/core/src/hooks/useDateField.ts index 243e75ba..372bcdad 100644 --- a/packages/x/src/useDateField.ts +++ b/packages/core/src/hooks/useDateField.ts @@ -1,11 +1,12 @@ import { useCallback, useContext } from 'react'; -import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import { DateFieldI18nContext } from './DateFieldI18n'; -import { formatDate } from './formatDate'; import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; +import { FieldConfig } from './useField'; +import { useFieldValidator } from './useFieldValidator'; +import { formatDate } from '../utils/formatDate'; dayjs.extend(customParseFormat); diff --git a/packages/x/src/useDecimalField.ts b/packages/core/src/hooks/useDecimalField.ts similarity index 93% rename from packages/x/src/useDecimalField.ts rename to packages/core/src/hooks/useDecimalField.ts index 35864f41..04d3528f 100644 --- a/packages/x/src/useDecimalField.ts +++ b/packages/core/src/hooks/useDecimalField.ts @@ -1,9 +1,10 @@ import { useCallback, useContext } from 'react'; -import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; import { DecimalFieldI18nContext } from './DecimalFieldI18n'; -import { formatDecimal } from './formatDecimal'; import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; +import { FieldConfig } from './useField'; +import { useFieldValidator } from './useFieldValidator'; +import { formatDecimal } from '../utils/formatDecimal'; const DECIMAL_REGEX = /^\d*\.?\d*$/; export const defaultPrecision = 2; diff --git a/packages/x/src/useIntegerField.ts b/packages/core/src/hooks/useIntegerField.ts similarity index 91% rename from packages/x/src/useIntegerField.ts rename to packages/core/src/hooks/useIntegerField.ts index 8800b467..62ea8097 100644 --- a/packages/x/src/useIntegerField.ts +++ b/packages/core/src/hooks/useIntegerField.ts @@ -1,9 +1,10 @@ import { useCallback, useContext } from 'react'; -import { FieldConfig, useFieldValidator } from '@reactive-forms/core'; -import { formatInteger } from './formatInteger'; import { IntegerFieldI18nContext } from './IntegerFieldI18n'; import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; +import { FieldConfig } from './useField'; +import { useFieldValidator } from './useFieldValidator'; +import { formatInteger } from '../utils/formatInteger'; const INTEGER_REGEX = /^-?\d+$/; diff --git a/packages/x/src/useStringField.ts b/packages/core/src/hooks/useStringField.ts similarity index 88% rename from packages/x/src/useStringField.ts rename to packages/core/src/hooks/useStringField.ts index fdf49520..417da632 100644 --- a/packages/x/src/useStringField.ts +++ b/packages/core/src/hooks/useStringField.ts @@ -1,7 +1,9 @@ import { useCallback, useContext } from 'react'; -import { FieldConfig, FieldContext, useField, useFieldValidator } from '@reactive-forms/core'; import { StringFieldI18nContext } from './StringFieldI18n'; +import { FieldConfig, useField } from './useField'; +import { useFieldValidator } from './useFieldValidator'; +import { FieldContext } from '../typings/FieldContext'; export type StringFieldConfig = FieldConfig & { required?: boolean; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 826e59ed..bd80348b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,6 +25,23 @@ export * from './hooks/useValidationRegistry'; export * from './hooks/usePluginAssertion'; export * from './hooks/useFieldValueArray'; +export * from './hooks/DecimalFieldI18n'; +export * from './hooks/useDecimalField'; + +export * from './hooks/DateFieldI18n'; +export * from './hooks/useDateField'; + +export * from './hooks/StringFieldI18n'; +export * from './hooks/useStringField'; + +export * from './hooks/BooleanFieldI18n'; +export * from './hooks/useBooleanField'; + +export * from './hooks/IntegerFieldI18n'; +export * from './hooks/useIntegerField'; + +export * from './hooks/useConverterField'; + // typings export * from './typings/FieldContext'; export * from './typings/FieldValidator'; diff --git a/packages/x/src/formatDate.ts b/packages/core/src/utils/formatDate.ts similarity index 100% rename from packages/x/src/formatDate.ts rename to packages/core/src/utils/formatDate.ts diff --git a/packages/x/src/formatDecimal.ts b/packages/core/src/utils/formatDecimal.ts similarity index 100% rename from packages/x/src/formatDecimal.ts rename to packages/core/src/utils/formatDecimal.ts diff --git a/packages/x/src/formatInteger.ts b/packages/core/src/utils/formatInteger.ts similarity index 100% rename from packages/x/src/formatInteger.ts rename to packages/core/src/utils/formatInteger.ts diff --git a/packages/x/tests/useBooleanField.test.tsx b/packages/core/tests/hooks/useBooleanField.test.tsx similarity index 91% rename from packages/x/tests/useBooleanField.test.tsx rename to packages/core/tests/hooks/useBooleanField.test.tsx index 57dabc51..ecc6d95b 100644 --- a/packages/x/tests/useBooleanField.test.tsx +++ b/packages/core/tests/hooks/useBooleanField.test.tsx @@ -1,9 +1,15 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { BooleanFieldI18n, BooleanFieldI18nContextProvider, defaultBooleanFieldI18n } from '../src/BooleanFieldI18n'; -import { BooleanFieldConfig, useBooleanField } from '../src/useBooleanField'; +import { + BooleanFieldConfig, + BooleanFieldI18n, + BooleanFieldI18nContextProvider, + defaultBooleanFieldI18n, + ReactiveFormProvider, + useBooleanField, + useForm, +} from '../../src'; type Config = Omit & { initialValue?: boolean; diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/core/tests/hooks/useConverterField.test.tsx similarity index 97% rename from packages/x/tests/useConverterField.test.tsx rename to packages/core/tests/hooks/useConverterField.test.tsx index 66854721..109e41c4 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/core/tests/hooks/useConverterField.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { ConversionError, useConverterField } from '../src/useConverterField'; +import { ConversionError, ReactiveFormProvider, useConverterField, useForm } from '../../src'; const defaultParse = (text: string) => { const parsingResult = Number.parseInt(text); diff --git a/packages/x/tests/useDateField.test.tsx b/packages/core/tests/hooks/useDateField.test.tsx similarity index 95% rename from packages/x/tests/useDateField.test.tsx rename to packages/core/tests/hooks/useDateField.test.tsx index aa8f7f5f..bcdaee72 100644 --- a/packages/x/tests/useDateField.test.tsx +++ b/packages/core/tests/hooks/useDateField.test.tsx @@ -1,10 +1,16 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { DateFieldI18n, DateFieldI18nContextProvider, defaultDateFieldI18n } from '../src/DateFieldI18n'; -import { formatDate } from '../src/formatDate'; -import { DateFieldConfig, useDateField } from '../src/useDateField'; +import { + DateFieldConfig, + DateFieldI18n, + DateFieldI18nContextProvider, + defaultDateFieldI18n, + ReactiveFormProvider, + useDateField, + useForm, +} from '../../src'; +import { formatDate } from '../../src/utils/formatDate'; type Config = Omit & { initialValue?: Date | null; diff --git a/packages/x/tests/useDecimalField.test.tsx b/packages/core/tests/hooks/useDecimalField.test.tsx similarity index 95% rename from packages/x/tests/useDecimalField.test.tsx rename to packages/core/tests/hooks/useDecimalField.test.tsx index 4235c366..42ffe836 100644 --- a/packages/x/tests/useDecimalField.test.tsx +++ b/packages/core/tests/hooks/useDecimalField.test.tsx @@ -1,10 +1,17 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { DecimalFieldI18n, DecimalFieldI18nContextProvider, defaultDecimalFieldI18n } from '../src/DecimalFieldI18n'; -import { formatDecimal } from '../src/formatDecimal'; -import { DecimalFieldConfig, defaultPrecision, useDecimalField } from '../src/useDecimalField'; +import { + DecimalFieldConfig, + DecimalFieldI18n, + DecimalFieldI18nContextProvider, + defaultDecimalFieldI18n, + defaultPrecision, + ReactiveFormProvider, + useDecimalField, + useForm, +} from '../../src'; +import { formatDecimal } from '../../src/utils/formatDecimal'; type Config = Omit & { initialValue?: number | null; diff --git a/packages/x/tests/useIntegerField.test.tsx b/packages/core/tests/hooks/useIntegerField.test.tsx similarity index 95% rename from packages/x/tests/useIntegerField.test.tsx rename to packages/core/tests/hooks/useIntegerField.test.tsx index 88767015..2ec7e758 100644 --- a/packages/x/tests/useIntegerField.test.tsx +++ b/packages/core/tests/hooks/useIntegerField.test.tsx @@ -1,9 +1,15 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { defaultIntegerFieldI18n, IntegerFieldI18n, IntegerFieldI18nContextProvider } from '../src/IntegerFieldI18n'; -import { IntegerFieldConfig, useIntegerField } from '../src/useIntegerField'; +import { + defaultIntegerFieldI18n, + IntegerFieldConfig, + IntegerFieldI18n, + IntegerFieldI18nContextProvider, + ReactiveFormProvider, + useForm, + useIntegerField, +} from '../../src'; type Config = Omit & { initialValue?: number | null; diff --git a/packages/x/tests/useStringField.test.tsx b/packages/core/tests/hooks/useStringField.test.tsx similarity index 96% rename from packages/x/tests/useStringField.test.tsx rename to packages/core/tests/hooks/useStringField.test.tsx index bf296ea9..01523f34 100644 --- a/packages/x/tests/useStringField.test.tsx +++ b/packages/core/tests/hooks/useStringField.test.tsx @@ -1,9 +1,15 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { defaultStringFieldI18n, StringFieldI18n, StringFieldI18nContextProvider } from '../src/StringFieldI18n'; -import { StringFieldConfig, useStringField } from '../src/useStringField'; +import { + defaultStringFieldI18n, + ReactiveFormProvider, + StringFieldConfig, + StringFieldI18n, + StringFieldI18nContextProvider, + useForm, + useStringField, +} from '../../src'; type Config = Omit & { initialValue?: string; diff --git a/packages/x/tests/formatDate.test.ts b/packages/core/tests/utils/formatDate.test.ts similarity index 85% rename from packages/x/tests/formatDate.test.ts rename to packages/core/tests/utils/formatDate.test.ts index 32c7392d..0b311013 100644 --- a/packages/x/tests/formatDate.test.ts +++ b/packages/core/tests/utils/formatDate.test.ts @@ -1,4 +1,4 @@ -import { formatDate } from '../src/formatDate'; +import { formatDate } from '../../src/utils/formatDate'; describe('format date', () => { it('should format correctly without pickTime option', () => { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 46b5d03c..d3e9461a 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -19,6 +19,6 @@ "isolatedModules": true, "outDir": "dist" }, - "include": ["src", "tests"], + "include": ["src"], "exclude": ["node_modules"] } diff --git a/packages/x/.eslintignore b/packages/x/.eslintignore deleted file mode 100644 index b9404616..00000000 --- a/packages/x/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -*.config.js \ No newline at end of file diff --git a/packages/x/.eslintrc.json b/packages/x/.eslintrc.json deleted file mode 100644 index fcd7da38..00000000 --- a/packages/x/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@reactive-tools/eslint-config" -} diff --git a/packages/x/CHANGELOG.md b/packages/x/CHANGELOG.md deleted file mode 100644 index 23bdda3a..00000000 --- a/packages/x/CHANGELOG.md +++ /dev/null @@ -1,56 +0,0 @@ -# @reactive-forms/x - -## 0.10.4 - -### Patch Changes - -- 524f4d2: Created useConverterField hook in @reactive-forms/x package - -## 0.10.3 - -## 0.10.2 - -### Patch Changes - -- de9d96e: Configured provenance - -## 0.10.1 - -## 0.10.0 - -## 0.9.1 - -## 0.9.0 - -## 0.8.16 - -### Patch Changes - -- @reactive-forms/core@0.8.16 - -## 0.8.15 - -### Patch Changes - -- @reactive-forms/core@0.8.15 - -## 0.8.14 - -### Patch Changes - -- 708a17b: Test release script -- Updated dependencies [708a17b] - - @reactive-forms/core@0.8.14 - -## 0.8.13 - -### Patch Changes - -- @reactive-forms/core@0.8.13 - -## 0.8.12 - -### Patch Changes - -- Updated dependencies [b4ded44] - - @reactive-forms/core@0.8.12 diff --git a/packages/x/LICENSE b/packages/x/LICENSE deleted file mode 100644 index b5ac9ad2..00000000 --- a/packages/x/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Artiom Tretjakovas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/packages/x/README.md b/packages/x/README.md deleted file mode 100644 index f0737d92..00000000 --- a/packages/x/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@reactive-forms/x` - -> TODO: description - -## Usage - -``` -const x = require('@reactive-forms/x'); - -// TODO: DEMONSTRATE API -``` diff --git a/packages/x/aqu.config.json b/packages/x/aqu.config.json deleted file mode 100644 index 79ee5a56..00000000 --- a/packages/x/aqu.config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "buildOptions": { - "target": ["es2019", "chrome58", "firefox57", "safari11", "edge18", "node12"], - "preserveSymlinks": false - }, - "dtsBundleGeneratorOptions": { - "libraries": { - "importedLibraries": ["stocked", "react", "@reactive-forms/core", "pxth"], - "allowedTypesLibraries": [] - } - } -} diff --git a/packages/x/jest.config.js b/packages/x/jest.config.js deleted file mode 100644 index 0bbb8647..00000000 --- a/packages/x/jest.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @type {import('ts-jest').JestConfigWithTsJest} - */ -const config = { - transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|cjs|jsx)$'"], - preset: 'ts-jest', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs', 'json', 'node'], - collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx,cjs,mjs}'], - testMatch: ['/**/*.(spec|test).{ts,tsx,js,jsx,cjs,mjs}'], - testEnvironmentOptions: { - url: 'http://localhost', - }, - testEnvironment: 'jsdom', - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - tsconfig: './tsconfig.test.json', - }, - ], - }, -}; - -module.exports = config; diff --git a/packages/x/package.json b/packages/x/package.json deleted file mode 100644 index b00be66d..00000000 --- a/packages/x/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@reactive-forms/x", - "description": "Advanced Reactive Forms components for rich eXperience", - "version": "0.10.6", - "main": "dist/index.js", - "module": "dist/x.esm.js", - "types": "dist/x.d.ts", - "bugs": "https://github.com/fracht/reactive-forms/issues", - "homepage": "https://github.com/fracht/reactive-forms#readme", - "repository": "fracht/reactive-forms.git", - "author": "ReactiveForms team", - "license": "MIT", - "publishConfig": { - "access": "public", - "directory": "prepared-package" - }, - "scripts": { - "build": "aqu build && rimraf ./prepared-package && clean-publish", - "lint": "eslint .", - "lint:fix": "npm run lint --fix", - "start": "aqu watch", - "test": "jest --passWithNoTests", - "test:log-coverage": "jest --passWithNoTests --coverage --silent --ci --coverageReporters=text", - "test:watch": "jest --passWithNoTests --watch" - }, - "devDependencies": { - "@babel/core": "7.19.6", - "@reactive-forms/core": "workspace:*", - "@reactive-tools/eslint-config": "workspace:*", - "@testing-library/react": "13.4.0", - "@types/jest": "26.0.24", - "@types/lodash": "4.14.161", - "@types/react": "18.0.23", - "aqu": "0.4.3", - "jest": "29.2.2", - "react": "18.2.0", - "rimraf": "3.0.2", - "ts-jest": "29.0.3", - "tslib": "2.3.1", - "typescript": "4.8.4" - }, - "peerDependencies": { - "@reactive-forms/core": "< 1.0.0", - "react": ">=16" - }, - "files": [ - "dist" - ], - "source": "src/index.ts", - "dependencies": { - "dayjs": "^1.11.9", - "lodash": "4.17.21" - } -} diff --git a/packages/x/src/index.ts b/packages/x/src/index.ts deleted file mode 100644 index 8eb012ea..00000000 --- a/packages/x/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './plugin'; -export * from './useConverterField'; diff --git a/packages/x/src/plugin.ts b/packages/x/src/plugin.ts deleted file mode 100644 index 5bb1f848..00000000 --- a/packages/x/src/plugin.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FormConfig, FormShared, Plugin } from '@reactive-forms/core'; - -export const xPlugin: Plugin = { - token: Symbol.for('x'), - useConfigDecorator: (config: FormConfig) => config, - useBagDecorator: (form: FormShared) => form, -}; diff --git a/packages/x/tsconfig.json b/packages/x/tsconfig.json deleted file mode 100644 index 97f79bdb..00000000 --- a/packages/x/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "module": "esnext", - "lib": ["dom", "esnext"], - "moduleResolution": "node", - "jsx": "react", - "sourceMap": true, - "declaration": true, - "esModuleInterop": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "allowSyntheticDefaultImports": true, - "noFallthroughCasesInSwitch": true, - "rootDir": "./src", - "strict": true, - "importHelpers": true - }, - "include": ["src", "tests"] -} diff --git a/packages/x/tsconfig.test.json b/packages/x/tsconfig.test.json deleted file mode 100644 index 0c2dfa50..00000000 --- a/packages/x/tsconfig.test.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "commonjs", - "noImplicitReturns": false, - "noImplicitThis": false, - "noImplicitAny": false, - "strictNullChecks": false, - "suppressImplicitAnyIndexErrors": false, - "noUnusedLocals": false, - "noUnusedParameters": false, - "strict": false, - "alwaysStrict": false - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c63c0ac..5c8db010 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,9 @@ importers: packages/core: dependencies: + dayjs: + specifier: ^1.11.9 + version: 1.11.9 lodash: specifier: 4.17.21 version: 4.17.21 From 80b15f8a509f617e79ed1ff3af5e79a479fd6eff Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 13 Sep 2023 16:56:32 +0300 Subject: [PATCH 68/70] Updated jest version --- packages/core/package.json | 8 +- packages/core/tsconfig.json | 7 +- packages/core/tsconfig.test.json | 1 + pnpm-lock.yaml | 942 ++++++++++++++++++++++++++++++- 4 files changed, 935 insertions(+), 23 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 460c5c1a..cabc42e9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,16 +34,16 @@ "devDependencies": { "@babel/core": "7.19.6", "@reactive-tools/eslint-config": "workspace:*", - "@testing-library/react": "13.4.0", - "@types/jest": "26.0.24", + "@testing-library/react": "14.0.0", + "@types/jest": "29.5.4", "@types/lodash": "4.14.161", "@types/node": "^18.11.18", "@types/react": "18.0.23", - "jest": "29.2.2", + "jest": "29.7.0", "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "3.0.2", - "ts-jest": "29.0.3", + "ts-jest": "29.1.1", "tslib": "2.3.1", "typescript": "4.8.4", "yup": "0.32.9" diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d3e9461a..36fe341d 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -8,7 +8,6 @@ "esModuleInterop": true, "lib": ["es2022", "DOM"], "noLib": false, - "rootDir": ".", "skipLibCheck": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", @@ -17,8 +16,8 @@ "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, "isolatedModules": true, - "outDir": "dist" + "outDir": "dist", + "rootDir": "src" }, - "include": ["src"], - "exclude": ["node_modules"] + "include": ["src"] } diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json index effc4db5..40a57af3 100644 --- a/packages/core/tsconfig.test.json +++ b/packages/core/tsconfig.test.json @@ -1,4 +1,5 @@ { + "extends": "../tsconfig.json", "compilerOptions": { "module": "commonjs", "noImplicitReturns": false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c8db010..660fe7a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,7 +71,7 @@ importers: version: 0.4.3(@babel/core@7.19.6) jest: specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) + version: 29.2.2 react: specifier: 18.2.0 version: 18.2.0 @@ -114,11 +114,11 @@ importers: specifier: workspace:* version: link:../../tools/eslint-config '@testing-library/react': - specifier: 13.4.0 - version: 13.4.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.0.0 + version: 14.0.0(react-dom@18.2.0)(react@18.2.0) '@types/jest': - specifier: 26.0.24 - version: 26.0.24 + specifier: 29.5.4 + version: 29.5.4 '@types/lodash': specifier: 4.14.161 version: 4.14.161 @@ -129,8 +129,8 @@ importers: specifier: 18.0.23 version: 18.0.23 jest: - specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) + specifier: 29.7.0 + version: 29.7.0(@types/node@18.11.18) react: specifier: 18.2.0 version: 18.2.0 @@ -141,8 +141,8 @@ importers: specifier: 3.0.2 version: 3.0.2 ts-jest: - specifier: 29.0.3 - version: 29.0.3(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.2.2)(typescript@4.8.4) + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.7.0)(typescript@4.8.4) tslib: specifier: 2.3.1 version: 2.3.1 @@ -195,7 +195,7 @@ importers: version: 5.0.0 jest: specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) + version: 29.2.2 jest-environment-jsdom: specifier: 29.2.1 version: 29.2.1 @@ -1881,6 +1881,18 @@ packages: slash: 3.0.0 dev: true + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/core@29.2.2: resolution: {integrity: sha512-susVl8o2KYLcZhhkvSB+b7xX575CX3TmSvxfeDjpRko7KmT89rHkXj6XkDkNpSeFMBzIENw5qIchO9HC9Sem+A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1923,6 +1935,49 @@ packages: - ts-node dev: true + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.5.0 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.11.18) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/environment@29.2.2: resolution: {integrity: sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1933,6 +1988,16 @@ packages: jest-mock: 29.2.2 dev: true + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + jest-mock: 29.7.0 + dev: true + /@jest/expect-utils@29.2.2: resolution: {integrity: sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1940,6 +2005,13 @@ packages: jest-get-type: 29.2.0 dev: true + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + /@jest/expect@29.2.2: resolution: {integrity: sha512-zwblIZnrIVt8z/SiEeJ7Q9wKKuB+/GS4yZe9zw7gMqfGf4C5hBLGrVyxu1SzDbVSqyMSlprKl3WL1r80cBNkgg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1950,6 +2022,16 @@ packages: - supports-color dev: true + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/fake-timers@29.2.2: resolution: {integrity: sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1962,6 +2044,18 @@ packages: jest-util: 29.2.1 dev: true + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.11.18 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /@jest/globals@29.2.2: resolution: {integrity: sha512-/nt+5YMh65kYcfBhj38B3Hm0Trk4IsuMXNDGKE/swp36yydBWfz3OXkLqkSvoAtPW8IJMSJDFCbTM2oj5SNprw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1974,6 +2068,18 @@ packages: - supports-color dev: true + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/reporters@29.2.2: resolution: {integrity: sha512-AzjL2rl2zJC0njIzcooBvjA4sJjvdoq98sDuuNs4aNugtLPSQ+91nysGKRF0uY1to5k0MdGMdOBggUsPqvBcpA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2011,6 +2117,43 @@ packages: - supports-color dev: true + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 18.11.18 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 6.0.0 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@29.0.0: resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2018,6 +2161,13 @@ packages: '@sinclair/typebox': 0.24.51 dev: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jest/source-map@29.2.0: resolution: {integrity: sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2027,6 +2177,15 @@ packages: graceful-fs: 4.2.10 dev: true + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.10 + dev: true + /@jest/test-result@29.2.1: resolution: {integrity: sha512-lS4+H+VkhbX6z64tZP7PAUwPqhwj3kbuEHcaLuaBuB+riyaX7oa1txe0tXgrFj5hRWvZKvqO7LZDlNWeJ7VTPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2037,6 +2196,16 @@ packages: collect-v8-coverage: 1.0.1 dev: true + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + dev: true + /@jest/test-sequencer@29.2.2: resolution: {integrity: sha512-Cuc1znc1pl4v9REgmmLf0jBd3Y65UXJpioGYtMr/JNpQEIGEzkmHhy6W6DLbSsXeUA13TDzymPv0ZGZ9jH3eIw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2047,6 +2216,16 @@ packages: slash: 3.0.0 dev: true + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/transform@29.2.2: resolution: {integrity: sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2070,6 +2249,29 @@ packages: - supports-color dev: true + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.19.6 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/types@26.6.2: resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} engines: {node: '>= 10.14.2'} @@ -2093,6 +2295,18 @@ packages: chalk: 4.1.2 dev: true + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.11.18 + '@types/yargs': 17.0.13 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping@0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} @@ -2131,6 +2345,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -2414,12 +2635,28 @@ packages: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sinonjs/commons@1.8.3: resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==} dependencies: type-detect: 4.0.8 dev: true + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: true + /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} dependencies: @@ -2471,6 +2708,20 @@ packages: pretty-format: 27.5.1 dev: true + /@testing-library/dom@9.3.1: + resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} + engines: {node: '>=14'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.20.7 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.14 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + /@testing-library/react@13.4.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} engines: {node: '>=12'} @@ -2485,6 +2736,20 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@testing-library/react@14.0.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.20.7 + '@testing-library/dom': 9.3.1 + '@types/react-dom': 18.0.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -2494,6 +2759,10 @@ packages: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true + /@types/aria-query@5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + dev: true + /@types/babel__core@7.1.19: resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} dependencies: @@ -2558,6 +2827,13 @@ packages: pretty-format: 26.6.2 dev: true + /@types/jest@29.5.4: + resolution: {integrity: sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==} + dependencies: + expect: 29.2.2 + pretty-format: 29.2.1 + dev: true + /@types/jsdom@20.0.0: resolution: {integrity: sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==} dependencies: @@ -3039,7 +3315,7 @@ packages: fs-extra: 10.1.0 github-username: 6.0.0 inquirer: 7.3.3 - jest: 29.2.2(@types/node@18.11.18) + jest: 29.2.2 jest-watch-typeahead: 2.2.0(jest@29.2.2) lodash: 4.17.21 ora: 5.4.1 @@ -3075,6 +3351,12 @@ packages: deep-equal: 2.0.5 dev: true + /aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.0.5 + dev: true + /arr-diff@4.0.0: resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} engines: {node: '>=0.10.0'} @@ -3193,6 +3475,24 @@ packages: - supports-color dev: true + /babel-jest@29.7.0(@babel/core@7.19.6): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.19.6 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.1.19 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.19.6) + chalk: 4.1.2 + graceful-fs: 4.2.10 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -3216,6 +3516,16 @@ packages: '@types/babel__traverse': 7.18.2 dev: true + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.18.10 + '@babel/types': 7.19.4 + '@types/babel__core': 7.1.19 + '@types/babel__traverse': 7.18.2 + dev: true + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.19.6): resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -3283,6 +3593,17 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.6) dev: true + /babel-preset-jest@29.6.3(@babel/core@7.19.6): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.19.6 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.6) + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3668,6 +3989,10 @@ packages: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /copy-descriptor@0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} engines: {node: '>=0.10.0'} @@ -3728,6 +4053,25 @@ packages: p-map: 6.0.0 dev: true + /create-jest@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-config: 29.7.0(@types/node@18.11.18) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -3925,6 +4269,15 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-equal@2.0.5: resolution: {integrity: sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==} dependencies: @@ -4017,6 +4370,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4797,6 +5155,17 @@ packages: jest-util: 29.2.1 dev: true + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -5793,6 +6162,19 @@ packages: - supports-color dev: true + /istanbul-lib-instrument@6.0.0: + resolution: {integrity: sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.19.6 + '@babel/parser': 7.19.6 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-lib-report@3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} engines: {node: '>=8'} @@ -5829,6 +6211,15 @@ packages: p-limit: 3.1.0 dev: true + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + /jest-circus@29.2.2: resolution: {integrity: sha512-upSdWxx+Mh4DV7oueuZndJ1NVdgtTsqM4YgywHEx05UMH5nxxA2Qu9T9T9XVuR021XxqSoaKvSmmpAbjwwwxMw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5856,8 +6247,65 @@ packages: - supports-color dev: true - /jest-cli@29.2.2(@types/node@18.11.18): - resolution: {integrity: sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==} + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.3 + slash: 3.0.0 + stack-utils: 2.0.5 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.2.2: + resolution: {integrity: sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.2.2 + '@jest/test-result': 29.2.1 + '@jest/types': 29.2.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + import-local: 3.1.0 + jest-config: 29.2.2(@types/node@18.11.18) + jest-util: 29.2.1 + jest-validate: 29.2.2 + prompts: 2.4.2 + yargs: 17.6.0 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + + /jest-cli@29.2.2(@types/node@18.11.18): + resolution: {integrity: sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -5884,6 +6332,34 @@ packages: - ts-node dev: true + /jest-cli@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.11.18) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.11.18) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.6.0 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@29.2.2(@types/node@18.11.18): resolution: {integrity: sha512-Q0JX54a5g1lP63keRfKR8EuC7n7wwny2HoTRDb8cx78IwQOiaYUVZAdjViY3WcTxpR02rPUpvNVmZ1fkIlZPcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5923,6 +6399,46 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.19.6 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + babel-jest: 29.7.0(@babel/core@7.19.6) + chalk: 4.1.2 + ci-info: 3.5.0 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@26.6.2: resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==} engines: {node: '>= 10.14.2'} @@ -5943,6 +6459,16 @@ packages: pretty-format: 29.2.1 dev: true + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-docblock@29.2.0: resolution: {integrity: sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5950,6 +6476,13 @@ packages: detect-newline: 3.1.0 dev: true + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + /jest-each@29.2.1: resolution: {integrity: sha512-sGP86H/CpWHMyK3qGIGFCgP6mt+o5tu9qG4+tobl0LNdgny0aitLXs9/EBacLy3Bwqy+v4uXClqJgASJWcruYw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5961,6 +6494,17 @@ packages: pretty-format: 29.2.1 dev: true + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + /jest-environment-jsdom@29.2.1: resolution: {integrity: sha512-MipBdmrjgzEdQMkK7b7wBShOfv1VqO6FVwa9S43bZwKYLC4dlWnPiCgNpZX3ypNEpJO8EMpMhg4HrUkWUZXGiw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5996,6 +6540,18 @@ packages: jest-util: 29.2.1 dev: true + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /jest-get-type@26.3.0: resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==} engines: {node: '>= 10.14.2'} @@ -6006,6 +6562,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-haste-map@29.2.1: resolution: {integrity: sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6025,6 +6586,25 @@ packages: fsevents: 2.3.3 dev: true + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.5 + '@types/node': 18.11.18 + anymatch: 3.1.2 + fb-watchman: 2.0.2 + graceful-fs: 4.2.10 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /jest-leak-detector@29.2.1: resolution: {integrity: sha512-1YvSqYoiurxKOJtySc+CGVmw/e1v4yNY27BjWTVzp0aTduQeA7pdieLiW05wTYG/twlKOp2xS/pWuikQEmklug==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6033,6 +6613,14 @@ packages: pretty-format: 29.2.1 dev: true + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-matcher-utils@29.2.2: resolution: {integrity: sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6043,6 +6631,16 @@ packages: pretty-format: 29.2.1 dev: true + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-message-util@29.2.1: resolution: {integrity: sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6058,6 +6656,21 @@ packages: stack-utils: 2.0.5 dev: true + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.5 + dev: true + /jest-mock@29.2.2: resolution: {integrity: sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6067,6 +6680,15 @@ packages: jest-util: 29.2.1 dev: true + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + jest-util: 29.7.0 + dev: true + /jest-pnp-resolver@1.2.2(jest-resolve@29.2.2): resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} @@ -6079,11 +6701,28 @@ packages: jest-resolve: 29.2.2 dev: true + /jest-pnp-resolver@1.2.2(jest-resolve@29.7.0): + resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + /jest-regex-util@29.2.0: resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-resolve-dependencies@29.2.2: resolution: {integrity: sha512-wWOmgbkbIC2NmFsq8Lb+3EkHuW5oZfctffTGvwsA4JcJ1IRk8b2tg+hz44f0lngvRTeHvp3Kyix9ACgudHH9aQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6094,6 +6733,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-resolve@29.2.2: resolution: {integrity: sha512-3gaLpiC3kr14rJR3w7vWh0CBX2QAhfpfiQTwrFPvVrcHe5VUBtIXaR004aWE/X9B2CFrITOQAp5gxLONGrk6GA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6109,6 +6758,21 @@ packages: slash: 3.0.0 dev: true + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.2(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.1 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + /jest-runner@29.2.2: resolution: {integrity: sha512-1CpUxXDrbsfy9Hr9/1zCUUhT813kGGK//58HeIw/t8fa/DmkecEwZSWlb1N/xDKXg3uCFHQp1GCvlSClfImMxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6138,6 +6802,35 @@ packages: - supports-color dev: true + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.10 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + /jest-runtime@29.2.2: resolution: {integrity: sha512-TpR1V6zRdLynckKDIQaY41od4o0xWL+KOPUCZvJK2bu5P1UXhjobt5nJ2ICNeIxgyj9NGkO0aWgDqYPVhDNKjA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6168,6 +6861,36 @@ packages: - supports-color dev: true + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-snapshot@29.2.2: resolution: {integrity: sha512-GfKJrpZ5SMqhli3NJ+mOspDqtZfJBryGA8RIBxF+G+WbDoC7HCqKaeAss4Z/Sab6bAW11ffasx8/vGsj83jyjA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6200,6 +6923,34 @@ packages: - supports-color dev: true + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.19.6 + '@babel/generator': 7.19.6 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.19.6) + '@babel/plugin-syntax-typescript': 7.18.6(@babel/core@7.19.6) + '@babel/types': 7.19.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.6) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.10 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /jest-util@29.2.1: resolution: {integrity: sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6212,6 +6963,18 @@ packages: picomatch: 2.3.1 dev: true + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + ci-info: 3.5.0 + graceful-fs: 4.2.10 + picomatch: 2.3.1 + dev: true + /jest-validate@29.2.2: resolution: {integrity: sha512-eJXATaKaSnOuxNfs8CLHgdABFgUrd0TtWS8QckiJ4L/QVDF4KVbZFBBOwCBZHOS0Rc5fOxqngXeGXE3nGQkpQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6224,6 +6987,18 @@ packages: pretty-format: 29.2.1 dev: true + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + /jest-watch-typeahead@2.2.0(jest@29.2.2): resolution: {integrity: sha512-cM3Qbw9P+jUYxqUSt53KdDDFRVBG96XA6bsIAG0zffl/gUkNK/kjWcCX7R559BgPWs2/UDrsJHPIw2f6b0qZCw==} engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} @@ -6232,7 +7007,7 @@ packages: dependencies: ansi-escapes: 5.0.0 chalk: 4.1.2 - jest: 29.2.2(@types/node@18.11.18) + jest: 29.2.2 jest-regex-util: 29.2.0 jest-watcher: 29.2.2 slash: 4.0.0 @@ -6254,6 +7029,20 @@ packages: string-length: 4.0.2 dev: true + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + /jest-worker@29.2.1: resolution: {integrity: sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6264,6 +7053,36 @@ packages: supports-color: 8.1.1 dev: true + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 18.11.18 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.2.2: + resolution: {integrity: sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.2.2 + '@jest/types': 29.2.1 + import-local: 3.1.0 + jest-cli: 29.2.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /jest@29.2.2(@types/node@18.11.18): resolution: {integrity: sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6284,6 +7103,27 @@ packages: - ts-node dev: true + /jest@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.11.18) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /js-sdsl@4.1.5: resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} dev: true @@ -6377,6 +7217,12 @@ packages: hasBin: true dev: true + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: @@ -6676,6 +7522,11 @@ packages: hasBin: true dev: true + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /macos-release@2.5.0: resolution: {integrity: sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==} engines: {node: '>=6'} @@ -7396,6 +8247,15 @@ packages: react-is: 18.2.0 dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /prettyjson@1.2.5: resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} hasBin: true @@ -7448,6 +8308,10 @@ packages: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} + /pure-rand@6.0.3: + resolution: {integrity: sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==} + dev: true + /pxth@0.7.0: resolution: {integrity: sha512-V2bNJNl7FIuvjsnreT7EWaBiqD5akAuGrKAVxgarjmTSDcDIGKLhY39ZFBqMkSTN8OHEBM4pszNUN+rfrojMfw==} dependencies: @@ -7692,6 +8556,11 @@ packages: engines: {node: '>=10'} dev: true + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -7813,6 +8682,14 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true @@ -8362,7 +9239,7 @@ packages: bs-logger: 0.2.6 esbuild: 0.15.12 fast-json-stable-stringify: 2.1.0 - jest: 29.2.2(@types/node@18.11.18) + jest: 29.2.2 jest-util: 29.2.1 json5: 2.2.1 lodash.memoize: 4.1.2 @@ -8372,6 +9249,41 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.1.1(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.7.0)(typescript@4.8.4): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.19.6 + bs-logger: 0.2.6 + esbuild: 0.15.12 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.11.18) + jest-util: 29.2.1 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 4.8.4 + yargs-parser: 21.1.1 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} From 51a7ea5067a033d6dacaa0d92e13f050cbd0ac6a Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 13 Sep 2023 17:27:32 +0300 Subject: [PATCH 69/70] Rearranged core package structure --- .../BooleanField}/BooleanFieldI18n.tsx | 0 .../core/src/components/BooleanField/index.ts | 1 + .../BooleanField}/useBooleanField.ts | 6 +- .../src/components/ConverterField/index.ts | 6 ++ .../ConverterField}/useConverterField.ts | 10 +-- .../DateField}/DateFieldI18n.tsx | 2 +- .../core/src/components/DateField/index.ts | 1 + .../DateField}/useDateField.ts | 9 +-- .../DecimalField}/DecimalFieldI18n.tsx | 2 +- .../core/src/components/DecimalField/index.ts | 1 + .../DecimalField}/useDecimalField.ts | 13 ++-- .../core/src/components/{ => Field}/Field.tsx | 4 +- packages/core/src/components/Field/index.ts | 2 + .../{hooks => components/Field}/useField.ts | 10 +-- .../src/components/{ => Form}/FormContext.ts | 2 +- .../src/components/{ => Form}/FormPlugins.tsx | 4 +- .../{ => Form}/FormProxyProvider.tsx | 2 +- .../components/{ => Form}/ReactiveForm.tsx | 2 +- .../{ => Form}/ReactiveFormConsumer.tsx | 2 +- .../{ => Form}/ReactiveFormProvider.tsx | 2 +- packages/core/src/components/Form/index.ts | 6 ++ .../src/{hooks => components/Form}/useForm.ts | 36 ++++++----- .../Form}/useFormControl.ts | 8 +-- .../IntegerField}/IntegerFieldI18n.tsx | 0 .../core/src/components/IntegerField/index.ts | 1 + .../IntegerField}/useIntegerField.ts | 13 ++-- .../core/src/components/ObjectField/index.ts | 1 + .../ObjectField}/useObjectField.ts | 6 +- .../StringField}/StringFieldI18n.tsx | 0 .../core/src/components/StringField/index.ts | 1 + .../StringField}/useStringField.ts | 6 +- packages/core/src/helpers/index.ts | 20 ++++++ .../src/{hooks => helpers}/useArrayControl.ts | 0 .../src/{hooks => helpers}/useArrayField.ts | 2 +- .../{hooks => helpers}/useControlHandlers.ts | 0 .../src/{hooks => helpers}/useFieldError.ts | 0 .../src/{hooks => helpers}/useFieldTouched.ts | 0 .../{hooks => helpers}/useFieldValidator.ts | 0 .../src/{hooks => helpers}/useFieldValue.ts | 0 .../{hooks => helpers}/useFieldValueArray.ts | 0 .../src/{hooks => helpers}/useFormContext.ts | 2 +- .../src/{hooks => helpers}/useFormMeta.ts | 0 .../{hooks => helpers}/usePluginAssertion.ts | 2 +- .../core/src/{hooks => helpers}/usePlugins.ts | 4 +- .../useProxyInterception.ts | 2 +- .../src/{hooks => helpers}/useSubmitAction.ts | 0 .../useValidationRegistry.ts | 0 packages/core/src/index.ts | 63 +++---------------- packages/core/src/typings/FormHelpers.ts | 4 +- packages/core/src/typings/Plugin.ts | 2 +- packages/core/src/typings/index.ts | 13 ++++ packages/core/src/utils/index.ts | 17 +++++ 52 files changed, 166 insertions(+), 124 deletions(-) rename packages/core/src/{hooks => components/BooleanField}/BooleanFieldI18n.tsx (100%) create mode 100644 packages/core/src/components/BooleanField/index.ts rename packages/core/src/{hooks => components/BooleanField}/useBooleanField.ts (80%) create mode 100644 packages/core/src/components/ConverterField/index.ts rename packages/core/src/{hooks => components/ConverterField}/useConverterField.ts (91%) rename packages/core/src/{hooks => components/DateField}/DateFieldI18n.tsx (95%) create mode 100644 packages/core/src/components/DateField/index.ts rename packages/core/src/{hooks => components/DateField}/useDateField.ts (88%) rename packages/core/src/{hooks => components/DecimalField}/DecimalFieldI18n.tsx (95%) create mode 100644 packages/core/src/components/DecimalField/index.ts rename packages/core/src/{hooks => components/DecimalField}/useDecimalField.ts (87%) rename packages/core/src/components/{ => Field}/Field.tsx (72%) create mode 100644 packages/core/src/components/Field/index.ts rename packages/core/src/{hooks => components/Field}/useField.ts (61%) rename packages/core/src/components/{ => Form}/FormContext.ts (81%) rename packages/core/src/components/{ => Form}/FormPlugins.tsx (88%) rename packages/core/src/components/{ => Form}/FormProxyProvider.tsx (91%) rename packages/core/src/components/{ => Form}/ReactiveForm.tsx (92%) rename packages/core/src/components/{ => Form}/ReactiveFormConsumer.tsx (88%) rename packages/core/src/components/{ => Form}/ReactiveFormProvider.tsx (90%) create mode 100644 packages/core/src/components/Form/index.ts rename packages/core/src/{hooks => components/Form}/useForm.ts (90%) rename packages/core/src/{hooks => components/Form}/useFormControl.ts (81%) rename packages/core/src/{hooks => components/IntegerField}/IntegerFieldI18n.tsx (100%) create mode 100644 packages/core/src/components/IntegerField/index.ts rename packages/core/src/{hooks => components/IntegerField}/useIntegerField.ts (84%) create mode 100644 packages/core/src/components/ObjectField/index.ts rename packages/core/src/{hooks => components/ObjectField}/useObjectField.ts (90%) rename packages/core/src/{hooks => components/StringField}/StringFieldI18n.tsx (100%) create mode 100644 packages/core/src/components/StringField/index.ts rename packages/core/src/{hooks => components/StringField}/useStringField.ts (87%) create mode 100644 packages/core/src/helpers/index.ts rename packages/core/src/{hooks => helpers}/useArrayControl.ts (100%) rename packages/core/src/{hooks => helpers}/useArrayField.ts (91%) rename packages/core/src/{hooks => helpers}/useControlHandlers.ts (100%) rename packages/core/src/{hooks => helpers}/useFieldError.ts (100%) rename packages/core/src/{hooks => helpers}/useFieldTouched.ts (100%) rename packages/core/src/{hooks => helpers}/useFieldValidator.ts (100%) rename packages/core/src/{hooks => helpers}/useFieldValue.ts (100%) rename packages/core/src/{hooks => helpers}/useFieldValueArray.ts (100%) rename packages/core/src/{hooks => helpers}/useFormContext.ts (96%) rename packages/core/src/{hooks => helpers}/useFormMeta.ts (100%) rename packages/core/src/{hooks => helpers}/usePluginAssertion.ts (82%) rename packages/core/src/{hooks => helpers}/usePlugins.ts (81%) rename packages/core/src/{hooks => helpers}/useProxyInterception.ts (98%) rename packages/core/src/{hooks => helpers}/useSubmitAction.ts (100%) rename packages/core/src/{hooks => helpers}/useValidationRegistry.ts (100%) create mode 100644 packages/core/src/typings/index.ts create mode 100644 packages/core/src/utils/index.ts diff --git a/packages/core/src/hooks/BooleanFieldI18n.tsx b/packages/core/src/components/BooleanField/BooleanFieldI18n.tsx similarity index 100% rename from packages/core/src/hooks/BooleanFieldI18n.tsx rename to packages/core/src/components/BooleanField/BooleanFieldI18n.tsx diff --git a/packages/core/src/components/BooleanField/index.ts b/packages/core/src/components/BooleanField/index.ts new file mode 100644 index 00000000..3ca4b020 --- /dev/null +++ b/packages/core/src/components/BooleanField/index.ts @@ -0,0 +1 @@ +export { type BooleanFieldBag, type BooleanFieldConfig, useBooleanField } from './useBooleanField'; diff --git a/packages/core/src/hooks/useBooleanField.ts b/packages/core/src/components/BooleanField/useBooleanField.ts similarity index 80% rename from packages/core/src/hooks/useBooleanField.ts rename to packages/core/src/components/BooleanField/useBooleanField.ts index b7df1359..ff28d37c 100644 --- a/packages/core/src/hooks/useBooleanField.ts +++ b/packages/core/src/components/BooleanField/useBooleanField.ts @@ -1,9 +1,9 @@ import { useContext } from 'react'; import { BooleanFieldI18nContext } from './BooleanFieldI18n'; -import { FieldConfig, useField } from './useField'; -import { useFieldValidator } from './useFieldValidator'; -import { FieldContext } from '../typings/FieldContext'; +import { useFieldValidator } from '../../helpers/useFieldValidator'; +import { FieldContext } from '../../typings/FieldContext'; +import { FieldConfig, useField } from '../Field/useField'; export type BooleanFieldConfig = FieldConfig & { required?: boolean; diff --git a/packages/core/src/components/ConverterField/index.ts b/packages/core/src/components/ConverterField/index.ts new file mode 100644 index 00000000..e92556ce --- /dev/null +++ b/packages/core/src/components/ConverterField/index.ts @@ -0,0 +1,6 @@ +export { + useConverterField, + ConversionError, + type ConverterFieldBag, + type ConverterFieldConfig, +} from './useConverterField'; diff --git a/packages/core/src/hooks/useConverterField.ts b/packages/core/src/components/ConverterField/useConverterField.ts similarity index 91% rename from packages/core/src/hooks/useConverterField.ts rename to packages/core/src/components/ConverterField/useConverterField.ts index a05e723c..0d479a7c 100644 --- a/packages/core/src/hooks/useConverterField.ts +++ b/packages/core/src/components/ConverterField/useConverterField.ts @@ -1,11 +1,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import isObject from 'lodash/isObject'; -import { FieldConfig, useField } from './useField'; -import { useFieldValidator } from './useFieldValidator'; -import { FieldContext } from '../typings/FieldContext'; -import { FieldError } from '../typings/FieldError'; -import { FieldTouched } from '../typings/FieldTouched'; +import { useFieldValidator } from '../../helpers'; +import { FieldContext } from '../../typings/FieldContext'; +import { FieldError } from '../../typings/FieldError'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FieldConfig, useField } from '../Field/useField'; export class ConversionError extends Error { public constructor(errorMessage: string) { diff --git a/packages/core/src/hooks/DateFieldI18n.tsx b/packages/core/src/components/DateField/DateFieldI18n.tsx similarity index 95% rename from packages/core/src/hooks/DateFieldI18n.tsx rename to packages/core/src/components/DateField/DateFieldI18n.tsx index 07e31ef6..246b0db8 100644 --- a/packages/core/src/hooks/DateFieldI18n.tsx +++ b/packages/core/src/components/DateField/DateFieldI18n.tsx @@ -1,7 +1,7 @@ import React, { createContext, PropsWithChildren } from 'react'; import merge from 'lodash/merge'; -import { formatDate } from '../utils/formatDate'; +import { formatDate } from '../../utils/formatDate'; export type DateFieldI18n = { required: string; diff --git a/packages/core/src/components/DateField/index.ts b/packages/core/src/components/DateField/index.ts new file mode 100644 index 00000000..631ea7bd --- /dev/null +++ b/packages/core/src/components/DateField/index.ts @@ -0,0 +1 @@ +export { type DateFieldBag, type DateFieldConfig, useDateField } from './useDateField'; diff --git a/packages/core/src/hooks/useDateField.ts b/packages/core/src/components/DateField/useDateField.ts similarity index 88% rename from packages/core/src/hooks/useDateField.ts rename to packages/core/src/components/DateField/useDateField.ts index 372bcdad..02408dd3 100644 --- a/packages/core/src/hooks/useDateField.ts +++ b/packages/core/src/components/DateField/useDateField.ts @@ -3,10 +3,11 @@ import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import { DateFieldI18nContext } from './DateFieldI18n'; -import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; -import { FieldConfig } from './useField'; -import { useFieldValidator } from './useFieldValidator'; -import { formatDate } from '../utils/formatDate'; +import { useFieldValidator } from '../../helpers'; +import { formatDate } from '../../utils'; +import { ConversionError, ConverterFieldBag, useConverterField } from '../ConverterField'; +import { ValueConverter } from '../ConverterField/useConverterField'; +import { FieldConfig } from '../Field'; dayjs.extend(customParseFormat); diff --git a/packages/core/src/hooks/DecimalFieldI18n.tsx b/packages/core/src/components/DecimalField/DecimalFieldI18n.tsx similarity index 95% rename from packages/core/src/hooks/DecimalFieldI18n.tsx rename to packages/core/src/components/DecimalField/DecimalFieldI18n.tsx index 58d3b0b6..20d9cc66 100644 --- a/packages/core/src/hooks/DecimalFieldI18n.tsx +++ b/packages/core/src/components/DecimalField/DecimalFieldI18n.tsx @@ -1,7 +1,7 @@ import React, { createContext, PropsWithChildren } from 'react'; import merge from 'lodash/merge'; -import { formatDecimal } from '../utils/formatDecimal'; +import { formatDecimal } from '../../utils'; export type DecimalFieldI18n = { required: string; diff --git a/packages/core/src/components/DecimalField/index.ts b/packages/core/src/components/DecimalField/index.ts new file mode 100644 index 00000000..d802bcc5 --- /dev/null +++ b/packages/core/src/components/DecimalField/index.ts @@ -0,0 +1 @@ +export { type DecimalFieldBag, type DecimalFieldConfig, useDecimalField } from './useDecimalField'; diff --git a/packages/core/src/hooks/useDecimalField.ts b/packages/core/src/components/DecimalField/useDecimalField.ts similarity index 87% rename from packages/core/src/hooks/useDecimalField.ts rename to packages/core/src/components/DecimalField/useDecimalField.ts index 04d3528f..a45d0028 100644 --- a/packages/core/src/hooks/useDecimalField.ts +++ b/packages/core/src/components/DecimalField/useDecimalField.ts @@ -1,10 +1,15 @@ import { useCallback, useContext } from 'react'; import { DecimalFieldI18nContext } from './DecimalFieldI18n'; -import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; -import { FieldConfig } from './useField'; -import { useFieldValidator } from './useFieldValidator'; -import { formatDecimal } from '../utils/formatDecimal'; +import { useFieldValidator } from '../../helpers'; +import { formatDecimal } from '../../utils/formatDecimal'; +import { + ConversionError, + ConverterFieldBag, + useConverterField, + ValueConverter, +} from '../ConverterField/useConverterField'; +import { FieldConfig } from '../Field/useField'; const DECIMAL_REGEX = /^\d*\.?\d*$/; export const defaultPrecision = 2; diff --git a/packages/core/src/components/Field.tsx b/packages/core/src/components/Field/Field.tsx similarity index 72% rename from packages/core/src/components/Field.tsx rename to packages/core/src/components/Field/Field.tsx index 77715c2b..8f89ad57 100644 --- a/packages/core/src/components/Field.tsx +++ b/packages/core/src/components/Field/Field.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { FieldConfig, useField } from '../hooks/useField'; -import { FieldContext } from '../typings/FieldContext'; +import { FieldConfig, useField } from './useField'; +import { FieldContext } from '../../typings/FieldContext'; export type FieldProps = FieldConfig & { children: (ctx: FieldContext) => React.ReactNode; diff --git a/packages/core/src/components/Field/index.ts b/packages/core/src/components/Field/index.ts new file mode 100644 index 00000000..5519f716 --- /dev/null +++ b/packages/core/src/components/Field/index.ts @@ -0,0 +1,2 @@ +export { Field, type FieldProps } from './Field'; +export { type FieldConfig, useField } from './useField'; diff --git a/packages/core/src/hooks/useField.ts b/packages/core/src/components/Field/useField.ts similarity index 61% rename from packages/core/src/hooks/useField.ts rename to packages/core/src/components/Field/useField.ts index 765d9396..d09e8355 100644 --- a/packages/core/src/hooks/useField.ts +++ b/packages/core/src/components/Field/useField.ts @@ -1,10 +1,10 @@ import { Pxth } from 'pxth'; -import { useFieldError } from './useFieldError'; -import { useFieldTouched } from './useFieldTouched'; -import { FieldValidationProps, useFieldValidator } from './useFieldValidator'; -import { useFieldValue } from './useFieldValue'; -import { FieldContext } from '../typings/FieldContext'; +import { useFieldError } from '../../helpers/useFieldError'; +import { useFieldTouched } from '../../helpers/useFieldTouched'; +import { FieldValidationProps, useFieldValidator } from '../../helpers/useFieldValidator'; +import { useFieldValue } from '../../helpers/useFieldValue'; +import { FieldContext } from '../../typings/FieldContext'; export type FieldConfig = { name: Pxth; diff --git a/packages/core/src/components/FormContext.ts b/packages/core/src/components/Form/FormContext.ts similarity index 81% rename from packages/core/src/components/FormContext.ts rename to packages/core/src/components/Form/FormContext.ts index 83678533..7166ada4 100644 --- a/packages/core/src/components/FormContext.ts +++ b/packages/core/src/components/Form/FormContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import { FormShared } from '../hooks/useForm'; +import { FormShared } from './useForm'; export type FormContextType = FormShared; diff --git a/packages/core/src/components/FormPlugins.tsx b/packages/core/src/components/Form/FormPlugins.tsx similarity index 88% rename from packages/core/src/components/FormPlugins.tsx rename to packages/core/src/components/Form/FormPlugins.tsx index 9d4e8460..fe20c814 100644 --- a/packages/core/src/components/FormPlugins.tsx +++ b/packages/core/src/components/Form/FormPlugins.tsx @@ -1,8 +1,8 @@ import React, { createContext, PropsWithChildren, useRef } from 'react'; import invariant from 'tiny-invariant'; -import { Plugin } from '../typings/Plugin'; -import { PluginArray } from '../typings/PluginArray'; +import { Plugin } from '../../typings/Plugin'; +import { PluginArray } from '../../typings/PluginArray'; export const FormPluginsContext = createContext([]); diff --git a/packages/core/src/components/FormProxyProvider.tsx b/packages/core/src/components/Form/FormProxyProvider.tsx similarity index 91% rename from packages/core/src/components/FormProxyProvider.tsx rename to packages/core/src/components/Form/FormProxyProvider.tsx index 0c8331f4..6b59787a 100644 --- a/packages/core/src/components/FormProxyProvider.tsx +++ b/packages/core/src/components/Form/FormProxyProvider.tsx @@ -3,7 +3,7 @@ import { Pxth } from 'pxth'; import { StockProxy } from 'stocked'; import { FormContext } from './FormContext'; -import { useProxyInterception } from '../hooks/useProxyInterception'; +import { useProxyInterception } from '../../helpers'; export type FormProxyProviderProps = { proxy: StockProxy; diff --git a/packages/core/src/components/ReactiveForm.tsx b/packages/core/src/components/Form/ReactiveForm.tsx similarity index 92% rename from packages/core/src/components/ReactiveForm.tsx rename to packages/core/src/components/Form/ReactiveForm.tsx index 06c796e4..9de9021c 100644 --- a/packages/core/src/components/ReactiveForm.tsx +++ b/packages/core/src/components/Form/ReactiveForm.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { ReactiveFormProvider } from './ReactiveFormProvider'; -import { FormConfig, useForm } from '../hooks/useForm'; +import { FormConfig, useForm } from './useForm'; export type ReactiveFormProps = PropsWithChildren>; diff --git a/packages/core/src/components/ReactiveFormConsumer.tsx b/packages/core/src/components/Form/ReactiveFormConsumer.tsx similarity index 88% rename from packages/core/src/components/ReactiveFormConsumer.tsx rename to packages/core/src/components/Form/ReactiveFormConsumer.tsx index 7d3f6757..1efed0ee 100644 --- a/packages/core/src/components/ReactiveFormConsumer.tsx +++ b/packages/core/src/components/Form/ReactiveFormConsumer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormContextType } from './FormContext'; -import { useFormContext } from '../hooks/useFormContext'; +import { useFormContext } from '../../helpers'; export type ReactiveFormConsumerProps = { children: (shared: FormContextType) => React.ReactNode; diff --git a/packages/core/src/components/ReactiveFormProvider.tsx b/packages/core/src/components/Form/ReactiveFormProvider.tsx similarity index 90% rename from packages/core/src/components/ReactiveFormProvider.tsx rename to packages/core/src/components/Form/ReactiveFormProvider.tsx index 5ee499c4..4a58eb05 100644 --- a/packages/core/src/components/ReactiveFormProvider.tsx +++ b/packages/core/src/components/Form/ReactiveFormProvider.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { FormContext } from './FormContext'; -import { FormShared } from '../hooks/useForm'; +import { FormShared } from './useForm'; export type ReactiveFormProviderProps = PropsWithChildren<{ formBag: FormShared; diff --git a/packages/core/src/components/Form/index.ts b/packages/core/src/components/Form/index.ts new file mode 100644 index 00000000..da84c5f1 --- /dev/null +++ b/packages/core/src/components/Form/index.ts @@ -0,0 +1,6 @@ +export { useForm, type FormConfig, type FormShared } from './useForm'; +export { FormPlugins, FormPluginsContext, type FormPluginsProps } from './FormPlugins'; +export { FormProxyContext, FormProxyProvider, type FormProxyProviderProps } from './FormProxyProvider'; +export { ReactiveForm, type ReactiveFormProps } from './ReactiveForm'; +export { ReactiveFormConsumer, type ReactiveFormConsumerProps } from './ReactiveFormConsumer'; +export { ReactiveFormProvider, type ReactiveFormProviderProps } from './ReactiveFormProvider'; diff --git a/packages/core/src/hooks/useForm.ts b/packages/core/src/components/Form/useForm.ts similarity index 90% rename from packages/core/src/hooks/useForm.ts rename to packages/core/src/components/Form/useForm.ts index a3b8ab8e..443eda83 100644 --- a/packages/core/src/hooks/useForm.ts +++ b/packages/core/src/components/Form/useForm.ts @@ -9,22 +9,26 @@ import invariant from 'tiny-invariant'; import type { BaseSchema } from 'yup'; import { useFormControl } from './useFormControl'; -import { usePluginBagDecorators, usePluginConfigDecorators } from './usePlugins'; -import { useValidationRegistry, ValidationRegistryControl } from './useValidationRegistry'; -import { FieldError } from '../typings/FieldError'; -import { FieldPostProcessor } from '../typings/FieldPostProcessor'; -import { FieldTouched } from '../typings/FieldTouched'; -import { FieldValidator } from '../typings/FieldValidator'; -import { FormHelpers } from '../typings/FormHelpers'; -import { FormMeta } from '../typings/FormMeta'; -import { SubmitAction } from '../typings/SubmitAction'; -import { deepRemoveEmpty } from '../utils/deepRemoveEmpty'; -import { excludeOverlaps } from '../utils/excludeOverlaps'; -import { overrideMerge } from '../utils/overrideMerge'; -import { runYupSchema } from '../utils/runYupSchema'; -import { setNestedValues } from '../utils/setNestedValues'; -import { useRefCallback } from '../utils/useRefCallback'; -import { validatorResultToError } from '../utils/validatorResultToError'; +import { + usePluginBagDecorators, + usePluginConfigDecorators, + useValidationRegistry, + ValidationRegistryControl, +} from '../../helpers'; +import { FieldError } from '../../typings/FieldError'; +import { FieldPostProcessor } from '../../typings/FieldPostProcessor'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FieldValidator } from '../../typings/FieldValidator'; +import { FormHelpers } from '../../typings/FormHelpers'; +import { FormMeta } from '../../typings/FormMeta'; +import { SubmitAction } from '../../typings/SubmitAction'; +import { deepRemoveEmpty } from '../../utils/deepRemoveEmpty'; +import { excludeOverlaps } from '../../utils/excludeOverlaps'; +import { overrideMerge } from '../../utils/overrideMerge'; +import { runYupSchema } from '../../utils/runYupSchema'; +import { setNestedValues } from '../../utils/setNestedValues'; +import { useRefCallback } from '../../utils/useRefCallback'; +import { validatorResultToError } from '../../utils/validatorResultToError'; export type InitialFormStateConfig = { initialValues: Values; diff --git a/packages/core/src/hooks/useFormControl.ts b/packages/core/src/components/Form/useFormControl.ts similarity index 81% rename from packages/core/src/hooks/useFormControl.ts rename to packages/core/src/components/Form/useFormControl.ts index 0fc255ff..7ae839a1 100644 --- a/packages/core/src/hooks/useFormControl.ts +++ b/packages/core/src/components/Form/useFormControl.ts @@ -1,9 +1,9 @@ import { Stock, useStock } from 'stocked'; -import { ControlHandlers, useControlHandlers } from './useControlHandlers'; -import { FieldError } from '../typings/FieldError'; -import { FieldTouched } from '../typings/FieldTouched'; -import { FormMeta } from '../typings/FormMeta'; +import { ControlHandlers, useControlHandlers } from '../../helpers/useControlHandlers'; +import { FieldError } from '../../typings/FieldError'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FormMeta } from '../../typings/FormMeta'; export type FormControl = { formMeta: Stock; diff --git a/packages/core/src/hooks/IntegerFieldI18n.tsx b/packages/core/src/components/IntegerField/IntegerFieldI18n.tsx similarity index 100% rename from packages/core/src/hooks/IntegerFieldI18n.tsx rename to packages/core/src/components/IntegerField/IntegerFieldI18n.tsx diff --git a/packages/core/src/components/IntegerField/index.ts b/packages/core/src/components/IntegerField/index.ts new file mode 100644 index 00000000..adc6d45b --- /dev/null +++ b/packages/core/src/components/IntegerField/index.ts @@ -0,0 +1 @@ +export { type IntegerFieldBag, type IntegerFieldConfig, useIntegerField } from './useIntegerField'; diff --git a/packages/core/src/hooks/useIntegerField.ts b/packages/core/src/components/IntegerField/useIntegerField.ts similarity index 84% rename from packages/core/src/hooks/useIntegerField.ts rename to packages/core/src/components/IntegerField/useIntegerField.ts index 62ea8097..75fce837 100644 --- a/packages/core/src/hooks/useIntegerField.ts +++ b/packages/core/src/components/IntegerField/useIntegerField.ts @@ -1,10 +1,15 @@ import { useCallback, useContext } from 'react'; import { IntegerFieldI18nContext } from './IntegerFieldI18n'; -import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField'; -import { FieldConfig } from './useField'; -import { useFieldValidator } from './useFieldValidator'; -import { formatInteger } from '../utils/formatInteger'; +import { useFieldValidator } from '../../helpers/useFieldValidator'; +import { formatInteger } from '../../utils/formatInteger'; +import { + ConversionError, + ConverterFieldBag, + useConverterField, + ValueConverter, +} from '../ConverterField/useConverterField'; +import { FieldConfig } from '../Field/useField'; const INTEGER_REGEX = /^-?\d+$/; diff --git a/packages/core/src/components/ObjectField/index.ts b/packages/core/src/components/ObjectField/index.ts new file mode 100644 index 00000000..a197e28c --- /dev/null +++ b/packages/core/src/components/ObjectField/index.ts @@ -0,0 +1 @@ +export { type ObjectFieldConfig, type ObjectFieldProps, useObjectField } from './useObjectField'; diff --git a/packages/core/src/hooks/useObjectField.ts b/packages/core/src/components/ObjectField/useObjectField.ts similarity index 90% rename from packages/core/src/hooks/useObjectField.ts rename to packages/core/src/components/ObjectField/useObjectField.ts index 4fe9f628..9ee73395 100644 --- a/packages/core/src/hooks/useObjectField.ts +++ b/packages/core/src/components/ObjectField/useObjectField.ts @@ -1,9 +1,9 @@ import { useCallback } from 'react'; import { deepSet, Pxth } from 'pxth'; -import { FieldConfig, useField } from './useField'; -import { FieldError } from '../typings/FieldError'; -import { FieldTouched } from '../typings/FieldTouched'; +import { FieldError } from '../../typings/FieldError'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FieldConfig, useField } from '../Field/useField'; export type ObjectFieldConfig = {} & FieldConfig; diff --git a/packages/core/src/hooks/StringFieldI18n.tsx b/packages/core/src/components/StringField/StringFieldI18n.tsx similarity index 100% rename from packages/core/src/hooks/StringFieldI18n.tsx rename to packages/core/src/components/StringField/StringFieldI18n.tsx diff --git a/packages/core/src/components/StringField/index.ts b/packages/core/src/components/StringField/index.ts new file mode 100644 index 00000000..70457226 --- /dev/null +++ b/packages/core/src/components/StringField/index.ts @@ -0,0 +1 @@ +export { type StringFieldBag, type StringFieldConfig, useStringField } from './useStringField'; diff --git a/packages/core/src/hooks/useStringField.ts b/packages/core/src/components/StringField/useStringField.ts similarity index 87% rename from packages/core/src/hooks/useStringField.ts rename to packages/core/src/components/StringField/useStringField.ts index 417da632..de396aa9 100644 --- a/packages/core/src/hooks/useStringField.ts +++ b/packages/core/src/components/StringField/useStringField.ts @@ -1,9 +1,9 @@ import { useCallback, useContext } from 'react'; import { StringFieldI18nContext } from './StringFieldI18n'; -import { FieldConfig, useField } from './useField'; -import { useFieldValidator } from './useFieldValidator'; -import { FieldContext } from '../typings/FieldContext'; +import { useFieldValidator } from '../../helpers/useFieldValidator'; +import { FieldContext } from '../../typings/FieldContext'; +import { FieldConfig, useField } from '../Field/useField'; export type StringFieldConfig = FieldConfig & { required?: boolean; diff --git a/packages/core/src/helpers/index.ts b/packages/core/src/helpers/index.ts new file mode 100644 index 00000000..c30a51a1 --- /dev/null +++ b/packages/core/src/helpers/index.ts @@ -0,0 +1,20 @@ +export { type ArrayControl, type ArrayControlConfig, useArrayControl } from './useArrayControl'; +export { type ArrayFieldConfig, type ArrayFieldProps, useArrayField } from './useArrayField'; +export { type ControlHandlers, type ControlHandlersConfig, useControlHandlers } from './useControlHandlers'; +export { useFieldError } from './useFieldError'; +export { useFieldTouched } from './useFieldTouched'; +export { type FieldValidationProps, type UseFieldValidatorConfig, useFieldValidator } from './useFieldValidator'; +export { useFieldValue } from './useFieldValue'; +export { type FieldValueArrayConfig, useFieldValueArray } from './useFieldValueArray'; +export { useFormContext } from './useFormContext'; +export { type FormControl, type FormControlConfig, useFormControl } from '../components/Form/useFormControl'; +export { useFormMeta } from './useFormMeta'; +export { usePluginAssertion } from './usePluginAssertion'; +export { usePluginBagDecorators, usePluginConfigDecorators } from './usePlugins'; +export { useProxyInterception } from './useProxyInterception'; +export { useSubmitAction } from './useSubmitAction'; +export { + type ValidationRegistry, + type ValidationRegistryControl, + useValidationRegistry, +} from './useValidationRegistry'; diff --git a/packages/core/src/hooks/useArrayControl.ts b/packages/core/src/helpers/useArrayControl.ts similarity index 100% rename from packages/core/src/hooks/useArrayControl.ts rename to packages/core/src/helpers/useArrayControl.ts diff --git a/packages/core/src/hooks/useArrayField.ts b/packages/core/src/helpers/useArrayField.ts similarity index 91% rename from packages/core/src/hooks/useArrayField.ts rename to packages/core/src/helpers/useArrayField.ts index d38f6bbe..e34d5470 100644 --- a/packages/core/src/hooks/useArrayField.ts +++ b/packages/core/src/helpers/useArrayField.ts @@ -1,7 +1,7 @@ import { Pxth } from 'pxth'; import { ArrayControl, useArrayControl } from './useArrayControl'; -import { FieldConfig, useField } from './useField'; +import { FieldConfig, useField } from '../components/Field/useField'; import { FieldError } from '../typings/FieldError'; import { FieldTouched } from '../typings/FieldTouched'; diff --git a/packages/core/src/hooks/useControlHandlers.ts b/packages/core/src/helpers/useControlHandlers.ts similarity index 100% rename from packages/core/src/hooks/useControlHandlers.ts rename to packages/core/src/helpers/useControlHandlers.ts diff --git a/packages/core/src/hooks/useFieldError.ts b/packages/core/src/helpers/useFieldError.ts similarity index 100% rename from packages/core/src/hooks/useFieldError.ts rename to packages/core/src/helpers/useFieldError.ts diff --git a/packages/core/src/hooks/useFieldTouched.ts b/packages/core/src/helpers/useFieldTouched.ts similarity index 100% rename from packages/core/src/hooks/useFieldTouched.ts rename to packages/core/src/helpers/useFieldTouched.ts diff --git a/packages/core/src/hooks/useFieldValidator.ts b/packages/core/src/helpers/useFieldValidator.ts similarity index 100% rename from packages/core/src/hooks/useFieldValidator.ts rename to packages/core/src/helpers/useFieldValidator.ts diff --git a/packages/core/src/hooks/useFieldValue.ts b/packages/core/src/helpers/useFieldValue.ts similarity index 100% rename from packages/core/src/hooks/useFieldValue.ts rename to packages/core/src/helpers/useFieldValue.ts diff --git a/packages/core/src/hooks/useFieldValueArray.ts b/packages/core/src/helpers/useFieldValueArray.ts similarity index 100% rename from packages/core/src/hooks/useFieldValueArray.ts rename to packages/core/src/helpers/useFieldValueArray.ts diff --git a/packages/core/src/hooks/useFormContext.ts b/packages/core/src/helpers/useFormContext.ts similarity index 96% rename from packages/core/src/hooks/useFormContext.ts rename to packages/core/src/helpers/useFormContext.ts index 68cd194a..e6b683da 100644 --- a/packages/core/src/hooks/useFormContext.ts +++ b/packages/core/src/helpers/useFormContext.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import invariant from 'tiny-invariant'; -import { FormContext, FormContextType } from '../components/FormContext'; +import { FormContext, FormContextType } from '../components/Form/FormContext'; export const useFormContext = (): FormContextType => { const context = useContext(FormContext); diff --git a/packages/core/src/hooks/useFormMeta.ts b/packages/core/src/helpers/useFormMeta.ts similarity index 100% rename from packages/core/src/hooks/useFormMeta.ts rename to packages/core/src/helpers/useFormMeta.ts diff --git a/packages/core/src/hooks/usePluginAssertion.ts b/packages/core/src/helpers/usePluginAssertion.ts similarity index 82% rename from packages/core/src/hooks/usePluginAssertion.ts rename to packages/core/src/helpers/usePluginAssertion.ts index 972a858e..8a25d381 100644 --- a/packages/core/src/hooks/usePluginAssertion.ts +++ b/packages/core/src/helpers/usePluginAssertion.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import invariant from 'tiny-invariant'; -import { FormPluginsContext } from '../components/FormPlugins'; +import { FormPluginsContext } from '../components/Form/FormPlugins'; import { Plugin } from '../typings/Plugin'; export const usePluginAssertion = (plugin: Plugin, message: string) => { diff --git a/packages/core/src/hooks/usePlugins.ts b/packages/core/src/helpers/usePlugins.ts similarity index 81% rename from packages/core/src/hooks/usePlugins.ts rename to packages/core/src/helpers/usePlugins.ts index 1bbc9636..c5d8e748 100644 --- a/packages/core/src/hooks/usePlugins.ts +++ b/packages/core/src/helpers/usePlugins.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; -import { FormConfig, FormShared } from './useForm'; -import { FormPluginsContext } from '../components/FormPlugins'; +import { FormPluginsContext } from '../components/Form/FormPlugins'; +import { FormConfig, FormShared } from '../components/Form/useForm'; export const usePluginBagDecorators = (bag: FormShared, config: FormConfig): FormShared => { const plugins = useContext(FormPluginsContext); diff --git a/packages/core/src/hooks/useProxyInterception.ts b/packages/core/src/helpers/useProxyInterception.ts similarity index 98% rename from packages/core/src/hooks/useProxyInterception.ts rename to packages/core/src/helpers/useProxyInterception.ts index 218714a8..66b562ce 100644 --- a/packages/core/src/hooks/useProxyInterception.ts +++ b/packages/core/src/helpers/useProxyInterception.ts @@ -3,8 +3,8 @@ import { deepGet, deepSet, Pxth, relativePxth } from 'pxth'; import { intercept, StockProxy, useStockContext } from 'stocked'; import { useControlHandlers } from './useControlHandlers'; -import { FormShared } from './useForm'; import { useFormContext } from './useFormContext'; +import { FormShared } from '../components/Form/useForm'; import { FieldError } from '../typings/FieldError'; import { FieldValidator } from '../typings/FieldValidator'; diff --git a/packages/core/src/hooks/useSubmitAction.ts b/packages/core/src/helpers/useSubmitAction.ts similarity index 100% rename from packages/core/src/hooks/useSubmitAction.ts rename to packages/core/src/helpers/useSubmitAction.ts diff --git a/packages/core/src/hooks/useValidationRegistry.ts b/packages/core/src/helpers/useValidationRegistry.ts similarity index 100% rename from packages/core/src/hooks/useValidationRegistry.ts rename to packages/core/src/helpers/useValidationRegistry.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bd80348b..ed380528 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,55 +1,12 @@ -// main component -export { ReactiveForm as default } from './components/ReactiveForm'; - -// components +export * from './components/BooleanField'; +export * from './components/StringField'; +export * from './components/IntegerField'; +export * from './components/DecimalField'; +export * from './components/ConverterField'; +export * from './components/DateField'; export * from './components/Field'; -export * from './components/ReactiveFormProvider'; -export * from './components/ReactiveFormConsumer'; -export * from './components/FormProxyProvider'; -export * from './components/FormPlugins'; - -// hooks -export * from './hooks/useField'; -export * from './hooks/useArrayControl'; -export * from './hooks/useArrayField'; -export * from './hooks/useFieldValidator'; -export * from './hooks/useForm'; -export * from './hooks/useFormContext'; -export * from './hooks/useFormMeta'; -export * from './hooks/useFieldError'; -export * from './hooks/useFieldTouched'; -export * from './hooks/useFieldValue'; -export * from './hooks/useObjectField'; -export * from './hooks/useSubmitAction'; -export * from './hooks/useValidationRegistry'; -export * from './hooks/usePluginAssertion'; -export * from './hooks/useFieldValueArray'; - -export * from './hooks/DecimalFieldI18n'; -export * from './hooks/useDecimalField'; - -export * from './hooks/DateFieldI18n'; -export * from './hooks/useDateField'; - -export * from './hooks/StringFieldI18n'; -export * from './hooks/useStringField'; - -export * from './hooks/BooleanFieldI18n'; -export * from './hooks/useBooleanField'; - -export * from './hooks/IntegerFieldI18n'; -export * from './hooks/useIntegerField'; - -export * from './hooks/useConverterField'; +export * from './components/Form'; +export * from './components/ObjectField'; -// typings -export * from './typings/FieldContext'; -export * from './typings/FieldValidator'; -export * from './typings/FieldError'; -export * from './typings/FieldMeta'; -export * from './typings/FormMeta'; -export * from './typings/FieldTouched'; -export * from './typings/SubmitAction'; -export * from './typings/FormHelpers'; -export * from './typings/Plugin'; -export * from './typings/PluginArray'; +export * from './helpers'; +export * from './typings'; diff --git a/packages/core/src/typings/FormHelpers.ts b/packages/core/src/typings/FormHelpers.ts index c1a4b386..f7df836b 100644 --- a/packages/core/src/typings/FormHelpers.ts +++ b/packages/core/src/typings/FormHelpers.ts @@ -2,8 +2,8 @@ import { Pxth } from 'pxth'; import { FieldError } from './FieldError'; import { FieldPostProcessor } from './FieldPostProcessor'; -import { InitialFormState } from '../hooks/useForm'; -import { FormControl } from '../hooks/useFormControl'; +import { InitialFormState } from '../components/Form/useForm'; +import { FormControl } from '../components/Form/useFormControl'; export type FormHelpers = FormControl & { validateForm: (values: Values) => Promise>; diff --git a/packages/core/src/typings/Plugin.ts b/packages/core/src/typings/Plugin.ts index ff294add..e92d4b81 100644 --- a/packages/core/src/typings/Plugin.ts +++ b/packages/core/src/typings/Plugin.ts @@ -1,4 +1,4 @@ -import { FormConfig, FormShared } from '../hooks/useForm'; +import { FormConfig, FormShared } from '../components/Form/useForm'; export type Plugin = { token: Symbol; diff --git a/packages/core/src/typings/index.ts b/packages/core/src/typings/index.ts new file mode 100644 index 00000000..4261a468 --- /dev/null +++ b/packages/core/src/typings/index.ts @@ -0,0 +1,13 @@ +export * from './FieldContext'; +export * from './FieldError'; +export * from './FieldMeta'; +export * from './FieldPostProcessor'; +export * from './FieldTouched'; +export * from './FieldValidator'; +export * from './FormHelpers'; +export * from './FormMeta'; +export * from './index.js'; +export * from './NestedObject'; +export * from './Plugin'; +export * from './PluginArray'; +export * from './SubmitAction'; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 00000000..cbc67b66 --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,17 @@ +export * from './deepRemoveEmpty'; +export * from './excludeOverlaps'; +export * from './flattenObject'; +export * from './formatDate'; +export * from './formatDecimal'; +export * from './formatInteger'; +export * from './FunctionArray'; +export * from './getDifferenceMap'; +export * from './index.js'; +export * from './isYupError'; +export * from './joinPaths'; +export * from './overrideMerge'; +export * from './runYupSchema'; +export * from './setNestedValues'; +export * from './useRefCallback'; +export * from './validatorResultToError'; +export * from './yupToFormErrors'; From 641e2eeb7cd01d45412bbefb92e5567b4c5c8ec6 Mon Sep 17 00:00:00 2001 From: Aleksandras Sukelovic Date: Wed, 13 Sep 2023 17:38:22 +0300 Subject: [PATCH 70/70] Fixed tests --- packages/core/jest.config.js | 2 +- .../core/src/components/BooleanField/index.ts | 6 ++ .../core/src/components/DateField/index.ts | 6 ++ .../core/src/components/DecimalField/index.ts | 6 ++ .../core/src/components/IntegerField/index.ts | 6 ++ .../core/src/components/StringField/index.ts | 6 ++ packages/core/src/index.ts | 2 + packages/core/src/typings/index.ts | 1 - packages/core/src/utils/index.ts | 1 - .../tests/hooks/useControlHandlers.test.tsx | 3 +- .../core/tests/hooks/useDecimalField.test.tsx | 7 +- .../tests/hooks/useFormControl.proxy.test.tsx | 10 +- .../tests/hooks/useProxyInterception.test.tsx | 3 +- .../core/tests/hooks/useSubmitAction.test.tsx | 3 +- .../hooks/useValidationRegistry.test.tsx | 3 +- packages/core/tsconfig.test.json | 2 +- pnpm-lock.yaml | 101 ------------------ 17 files changed, 49 insertions(+), 119 deletions(-) diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 0bbb8647..e6227517 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -6,7 +6,7 @@ const config = { preset: 'ts-jest', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs', 'json', 'node'], collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx,cjs,mjs}'], - testMatch: ['/**/*.(spec|test).{ts,tsx,js,jsx,cjs,mjs}'], + testMatch: ['/tests/**/*.(spec|test).{ts,tsx,js,jsx,cjs,mjs}'], testEnvironmentOptions: { url: 'http://localhost', }, diff --git a/packages/core/src/components/BooleanField/index.ts b/packages/core/src/components/BooleanField/index.ts index 3ca4b020..82adc36e 100644 --- a/packages/core/src/components/BooleanField/index.ts +++ b/packages/core/src/components/BooleanField/index.ts @@ -1 +1,7 @@ export { type BooleanFieldBag, type BooleanFieldConfig, useBooleanField } from './useBooleanField'; +export { + type BooleanFieldI18n, + BooleanFieldI18nContextProvider, + type BooleanFieldI18nContextProviderProps, + defaultBooleanFieldI18n, +} from './BooleanFieldI18n'; diff --git a/packages/core/src/components/DateField/index.ts b/packages/core/src/components/DateField/index.ts index 631ea7bd..7af4cfae 100644 --- a/packages/core/src/components/DateField/index.ts +++ b/packages/core/src/components/DateField/index.ts @@ -1 +1,7 @@ export { type DateFieldBag, type DateFieldConfig, useDateField } from './useDateField'; +export { + type DateFieldI18n, + DateFieldI18nContextProvider, + type DateFieldI18nContextProviderProps, + defaultDateFieldI18n, +} from './DateFieldI18n'; diff --git a/packages/core/src/components/DecimalField/index.ts b/packages/core/src/components/DecimalField/index.ts index d802bcc5..10f61b04 100644 --- a/packages/core/src/components/DecimalField/index.ts +++ b/packages/core/src/components/DecimalField/index.ts @@ -1 +1,7 @@ export { type DecimalFieldBag, type DecimalFieldConfig, useDecimalField } from './useDecimalField'; +export { + type DecimalFieldI18n, + DecimalFieldI18nContextProvider, + type DecimalFieldI18nContextProviderProps, + defaultDecimalFieldI18n, +} from './DecimalFieldI18n'; diff --git a/packages/core/src/components/IntegerField/index.ts b/packages/core/src/components/IntegerField/index.ts index adc6d45b..fa200f31 100644 --- a/packages/core/src/components/IntegerField/index.ts +++ b/packages/core/src/components/IntegerField/index.ts @@ -1 +1,7 @@ export { type IntegerFieldBag, type IntegerFieldConfig, useIntegerField } from './useIntegerField'; +export { + type IntegerFieldI18n, + IntegerFieldI18nContextProvider, + type IntegerFieldI18nContextProviderProps, + defaultIntegerFieldI18n, +} from './IntegerFieldI18n'; diff --git a/packages/core/src/components/StringField/index.ts b/packages/core/src/components/StringField/index.ts index 70457226..95cdd02f 100644 --- a/packages/core/src/components/StringField/index.ts +++ b/packages/core/src/components/StringField/index.ts @@ -1 +1,7 @@ export { type StringFieldBag, type StringFieldConfig, useStringField } from './useStringField'; +export { + type StringFieldI18n, + StringFieldI18nContextProvider, + type StringFieldI18nContextProviderProps, + defaultStringFieldI18n, +} from './StringFieldI18n'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ed380528..c0f08266 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,3 +10,5 @@ export * from './components/ObjectField'; export * from './helpers'; export * from './typings'; + +export { ReactiveForm as default } from './components/Form/ReactiveForm'; diff --git a/packages/core/src/typings/index.ts b/packages/core/src/typings/index.ts index 4261a468..c466949f 100644 --- a/packages/core/src/typings/index.ts +++ b/packages/core/src/typings/index.ts @@ -6,7 +6,6 @@ export * from './FieldTouched'; export * from './FieldValidator'; export * from './FormHelpers'; export * from './FormMeta'; -export * from './index.js'; export * from './NestedObject'; export * from './Plugin'; export * from './PluginArray'; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index cbc67b66..1ac34df0 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -6,7 +6,6 @@ export * from './formatDecimal'; export * from './formatInteger'; export * from './FunctionArray'; export * from './getDifferenceMap'; -export * from './index.js'; export * from './isYupError'; export * from './joinPaths'; export * from './overrideMerge'; diff --git a/packages/core/tests/hooks/useControlHandlers.test.tsx b/packages/core/tests/hooks/useControlHandlers.test.tsx index c47e7e70..5ceb5055 100644 --- a/packages/core/tests/hooks/useControlHandlers.test.tsx +++ b/packages/core/tests/hooks/useControlHandlers.test.tsx @@ -2,8 +2,7 @@ import React, { PropsWithChildren } from 'react'; import { act, renderHook } from '@testing-library/react'; import { createPxth } from 'pxth'; -import { FormShared, ReactiveFormProvider, useForm } from '../../src'; -import { useControlHandlers } from '../../src/hooks/useControlHandlers'; +import { FormShared, ReactiveFormProvider, useControlHandlers, useForm } from '../../src'; const renderControlHandlers = () => { const { diff --git a/packages/core/tests/hooks/useDecimalField.test.tsx b/packages/core/tests/hooks/useDecimalField.test.tsx index 42ffe836..3f5fc3a2 100644 --- a/packages/core/tests/hooks/useDecimalField.test.tsx +++ b/packages/core/tests/hooks/useDecimalField.test.tsx @@ -6,7 +6,6 @@ import { DecimalFieldI18n, DecimalFieldI18nContextProvider, defaultDecimalFieldI18n, - defaultPrecision, ReactiveFormProvider, useDecimalField, useForm, @@ -52,7 +51,7 @@ describe('Decimal field', () => { it('Should format initial value correctly', () => { const [{ result }] = renderUseDecimalField(); - expect(result.current.text).toBe(formatDecimal(0, defaultPrecision)); + expect(result.current.text).toBe(formatDecimal(0, 2)); expect(result.current.value).toBe(0); }); @@ -168,7 +167,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.minValue(0.5, defaultPrecision)); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.minValue(0.5, 2)); }); act(() => { @@ -188,7 +187,7 @@ describe('Decimal field', () => { }); await waitFor(() => { - expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.maxValue(0.5, defaultPrecision)); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.maxValue(0.5, 2)); }); act(() => { diff --git a/packages/core/tests/hooks/useFormControl.proxy.test.tsx b/packages/core/tests/hooks/useFormControl.proxy.test.tsx index bd8e2d70..0d4297c2 100644 --- a/packages/core/tests/hooks/useFormControl.proxy.test.tsx +++ b/packages/core/tests/hooks/useFormControl.proxy.test.tsx @@ -3,8 +3,14 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react'; import { createPxth } from 'pxth'; import { MappingProxy, StockProxy } from 'stocked'; -import { FormProxyProvider, FormShared, ReactiveFormProvider, useForm, useFormContext } from '../../src'; -import { FormControlConfig } from '../../src/hooks/useFormControl'; +import { + FormControlConfig, + FormProxyProvider, + FormShared, + ReactiveFormProvider, + useForm, + useFormContext, +} from '../../src'; const renderFormContextWithProxy = >( config: FormControlConfig, diff --git a/packages/core/tests/hooks/useProxyInterception.test.tsx b/packages/core/tests/hooks/useProxyInterception.test.tsx index daa021a8..62521282 100644 --- a/packages/core/tests/hooks/useProxyInterception.test.tsx +++ b/packages/core/tests/hooks/useProxyInterception.test.tsx @@ -3,8 +3,7 @@ import { renderHook } from '@testing-library/react'; import { createPxth, getPxthSegments, Pxth } from 'pxth'; import { MappingProxy } from 'stocked'; -import { FieldError, FieldValidator, ReactiveFormProvider, useForm } from '../../src'; -import { useProxyInterception } from '../../src/hooks/useProxyInterception'; +import { FieldError, FieldValidator, ReactiveFormProvider, useForm, useProxyInterception } from '../../src'; type ProxyValue = { id: number; diff --git a/packages/core/tests/hooks/useSubmitAction.test.tsx b/packages/core/tests/hooks/useSubmitAction.test.tsx index 46b15278..8743eef0 100644 --- a/packages/core/tests/hooks/useSubmitAction.test.tsx +++ b/packages/core/tests/hooks/useSubmitAction.test.tsx @@ -1,8 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { act, renderHook } from '@testing-library/react'; -import ReactiveForm from '../../src'; -import { useSubmitAction } from '../../src/hooks/useSubmitAction'; +import ReactiveForm, { useSubmitAction } from '../../src'; describe('useSubmitAction', () => { it('should return default submit', async () => { diff --git a/packages/core/tests/hooks/useValidationRegistry.test.tsx b/packages/core/tests/hooks/useValidationRegistry.test.tsx index f0e4374f..da57d565 100644 --- a/packages/core/tests/hooks/useValidationRegistry.test.tsx +++ b/packages/core/tests/hooks/useValidationRegistry.test.tsx @@ -1,8 +1,7 @@ import { renderHook } from '@testing-library/react'; import { createPxth, getPxthSegments } from 'pxth'; -import { FieldError, FieldInnerError } from '../../src'; -import { useValidationRegistry } from '../../src/hooks/useValidationRegistry'; +import { FieldError, FieldInnerError, useValidationRegistry } from '../../src'; const renderUseValidationRegistry = () => { return renderHook(() => useValidationRegistry()); diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json index 40a57af3..9b92b823 100644 --- a/packages/core/tsconfig.test.json +++ b/packages/core/tsconfig.test.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.json", + "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", "noImplicitReturns": false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 660fe7a1..bd06dddd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,59 +219,6 @@ importers: version: 4.8.4 publishDirectory: prepared-package - packages/x: - dependencies: - dayjs: - specifier: ^1.11.9 - version: 1.11.9 - lodash: - specifier: 4.17.21 - version: 4.17.21 - devDependencies: - '@babel/core': - specifier: 7.19.6 - version: 7.19.6 - '@reactive-forms/core': - specifier: workspace:* - version: link:../core/prepared-package - '@reactive-tools/eslint-config': - specifier: workspace:* - version: link:../../tools/eslint-config - '@testing-library/react': - specifier: 13.4.0 - version: 13.4.0(react-dom@18.2.0)(react@18.2.0) - '@types/jest': - specifier: 26.0.24 - version: 26.0.24 - '@types/lodash': - specifier: 4.14.161 - version: 4.14.161 - '@types/react': - specifier: 18.0.23 - version: 18.0.23 - aqu: - specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6) - jest: - specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) - react: - specifier: 18.2.0 - version: 18.2.0 - rimraf: - specifier: 3.0.2 - version: 3.0.2 - ts-jest: - specifier: 29.0.3 - version: 29.0.3(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.2.2)(typescript@4.8.4) - tslib: - specifier: 2.3.1 - version: 2.3.1 - typescript: - specifier: 4.8.4 - version: 4.8.4 - publishDirectory: prepared-package - tools/eslint-config: dependencies: '@typescript-eslint/eslint-plugin': @@ -6304,34 +6251,6 @@ packages: - ts-node dev: true - /jest-cli@29.2.2(@types/node@18.11.18): - resolution: {integrity: sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.2.2 - '@jest/test-result': 29.2.1 - '@jest/types': 29.2.1 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 29.2.2(@types/node@18.11.18) - jest-util: 29.2.1 - jest-validate: 29.2.2 - prompts: 2.4.2 - yargs: 17.6.0 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest-cli@29.7.0(@types/node@18.11.18): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7083,26 +7002,6 @@ packages: - ts-node dev: true - /jest@29.2.2(@types/node@18.11.18): - resolution: {integrity: sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.2.2 - '@jest/types': 29.2.1 - import-local: 3.1.0 - jest-cli: 29.2.2(@types/node@18.11.18) - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest@29.7.0(@types/node@18.11.18): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}