From 509d3c6cf8fa75d1f01db2d9a39cc95fdef56838 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 27 Sep 2022 13:49:32 +0200 Subject: [PATCH] feat: enable dynamic validation --- .npmrc | 1 + src/components/CheckboxField/index.tsx | 14 +--- src/components/DateField/index.tsx | 17 ++-- src/components/RadioField/index.tsx | 14 +--- src/components/RichSelectField/index.tsx | 15 +--- src/components/SelectNumberField/index.tsx | 14 +--- src/components/SelectableCardField/index.tsx | 14 +--- src/components/TagsField/index.tsx | 14 +--- .../__stories__/index.stories.tsx | 26 +++++++ src/components/TextBoxField/index.tsx | 26 ++----- src/components/TimeField/index.tsx | 14 +--- src/components/ToggleField/index.tsx | 12 +-- src/hooks/__tests__/useField.spec.tsx | 15 ++++ src/hooks/index.ts | 1 + src/hooks/useField.ts | 77 +++++++++++++++++++ src/hooks/useValidation.ts | 9 +-- 16 files changed, 161 insertions(+), 122 deletions(-) create mode 100644 src/hooks/__tests__/useField.spec.tsx create mode 100644 src/hooks/useField.ts diff --git a/.npmrc b/.npmrc index 15fb6586c..29e0bba64 100644 --- a/.npmrc +++ b/.npmrc @@ -8,3 +8,4 @@ public-hoist-pattern[]='*xstyled*' public-hoist-pattern[]='*reakit*' public-hoist-pattern[]='*scaleway*' public-hoist-pattern[]='*react-select*' +public-hoist-pattern[]='*eslint*' diff --git a/src/components/CheckboxField/index.tsx b/src/components/CheckboxField/index.tsx index f631ca332..a3ff44080 100644 --- a/src/components/CheckboxField/index.tsx +++ b/src/components/CheckboxField/index.tsx @@ -1,9 +1,7 @@ import { Checkbox } from '@scaleway/ui' import { FieldState } from 'final-form' import React, { ComponentProps, ReactNode, Ref, forwardRef } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -50,16 +48,10 @@ export const CheckboxField = forwardRef( ): JSX.Element => { const { getError } = useErrors() - const validateFn = useValidation({ - validate, - validators: pickValidators({ - required, - }), - }) - const { input, meta } = useField(name, { + required, type: 'checkbox', - validate: validateFn, + validate, value, }) diff --git a/src/components/DateField/index.tsx b/src/components/DateField/index.tsx index 3087d0f21..c2060f6f9 100644 --- a/src/components/DateField/index.tsx +++ b/src/components/DateField/index.tsx @@ -1,9 +1,7 @@ import { DateInput } from '@scaleway/ui' import { FieldState } from 'final-form' import React, { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -56,19 +54,14 @@ export const DateField = ({ formatOnBlur, }: DateFieldProps) => { const { getError } = useErrors() - const validateFn = useValidation({ - validate, - validators: pickValidators({ - maxDate, - minDate, - required, - }), - }) const { input, meta } = useField(name, { formatOnBlur, initialValue, - validate: validateFn, + maxDate, + minDate, + required, + validate, value: inputVal, }) diff --git a/src/components/RadioField/index.tsx b/src/components/RadioField/index.tsx index f387edbc1..297d6edb5 100644 --- a/src/components/RadioField/index.tsx +++ b/src/components/RadioField/index.tsx @@ -1,9 +1,7 @@ import { Radio } from '@scaleway/ui' import { FieldState } from 'final-form' import React, { ComponentProps, ReactNode } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -40,16 +38,10 @@ export const RadioField = ({ }: RadioFieldProps): JSX.Element => { const { getError } = useErrors() - const validateFn = useValidation({ - validate, - validators: pickValidators({ - required, - }), - }) - const { input, meta } = useField(name, { + required, type: 'radio', - validate: validateFn, + validate, value, }) diff --git a/src/components/RichSelectField/index.tsx b/src/components/RichSelectField/index.tsx index 9b30f0ad5..768edc847 100644 --- a/src/components/RichSelectField/index.tsx +++ b/src/components/RichSelectField/index.tsx @@ -7,9 +7,7 @@ import React, { useCallback, useMemo, } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -92,13 +90,6 @@ export const RichSelectField = < noTopLabel, }: RichSelectFieldProps) => { const { getError } = useErrors() - const validate = useValidation({ - validators: pickValidators({ - maxLength, - minLength: minLength || required ? 1 : undefined, - required, - }), - }) const options = useMemo( () => @@ -167,9 +158,11 @@ export const RichSelectField = < const { input, meta } = useField(name, { format, formatOnBlur, + maxLength, + minLength: minLength || required ? 1 : undefined, multiple, parse, - validate, + required, value, }) diff --git a/src/components/SelectNumberField/index.tsx b/src/components/SelectNumberField/index.tsx index fdd8e9584..27fbb4744 100644 --- a/src/components/SelectNumberField/index.tsx +++ b/src/components/SelectNumberField/index.tsx @@ -1,8 +1,6 @@ import { SelectNumber } from '@scaleway/ui' import React, { ComponentProps, FocusEvent, FocusEventHandler } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { BaseFieldProps } from '../../types' type SelectNumberValue = NonNullable< @@ -53,16 +51,10 @@ export const SelectNumberField = ({ value, className, }: SelectNumberValueFieldProps) => { - const validateFn = useValidation({ - validate, - validators: pickValidators({ - required, - }), - }) - const { input } = useField(name, { + required, type: 'number', - validate: validateFn, + validate, value, }) diff --git a/src/components/SelectableCardField/index.tsx b/src/components/SelectableCardField/index.tsx index 44c7955bf..e22c7121b 100644 --- a/src/components/SelectableCardField/index.tsx +++ b/src/components/SelectableCardField/index.tsx @@ -1,9 +1,7 @@ import { SelectableCard } from '@scaleway/ui' import { FieldState } from 'final-form' import React, { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -56,16 +54,10 @@ export const SelectableCardField = ({ }: SelectableCardFieldProps): JSX.Element => { const { getError } = useErrors() - const validateFn = useValidation({ - validate, - validators: pickValidators({ - required, - }), - }) - const { input, meta } = useField(name, { + required, type: type ?? 'radio', - validate: validateFn, + validate, value, }) diff --git a/src/components/TagsField/index.tsx b/src/components/TagsField/index.tsx index b6f482a60..506b2440e 100644 --- a/src/components/TagsField/index.tsx +++ b/src/components/TagsField/index.tsx @@ -1,8 +1,6 @@ import { Tags } from '@scaleway/ui' import React, { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { BaseFieldProps } from '../../types' export type TagsFieldProps = BaseFieldProps & @@ -29,16 +27,10 @@ export const TagsField = ({ validate, variant, }: TagsFieldProps): JSX.Element => { - const validateFn = useValidation({ - validate, - validators: pickValidators({ - required, - }), - }) - const { input } = useField(name, { + required, type: 'text', - validate: validateFn, + validate, }) return ( diff --git a/src/components/TextBoxField/__stories__/index.stories.tsx b/src/components/TextBoxField/__stories__/index.stories.tsx index 07235e02b..c479eabd6 100644 --- a/src/components/TextBoxField/__stories__/index.stories.tsx +++ b/src/components/TextBoxField/__stories__/index.stories.tsx @@ -1,3 +1,4 @@ +import { Checkbox } from '@scaleway/ui' import { Meta, Story } from '@storybook/react' import React, { ComponentProps } from 'react' import { Form, Submit, TextBoxField } from '../..' @@ -53,6 +54,31 @@ Required.args = { required: true, } +export const DynamicRequired: Story< + ComponentProps +> = args => { + const [isRequired, setIsRequired] = React.useState(true) + + return ( + <> + setIsRequired(!isRequired)} + > + Is field required? + + +
+ Submit +
+ + ) +} + +DynamicRequired.args = { + name: 'required', +} + export const MinMaxLength: Story< ComponentProps > = args => ( diff --git a/src/components/TextBoxField/index.tsx b/src/components/TextBoxField/index.tsx index e8a2b7d56..084a7fb0b 100644 --- a/src/components/TextBoxField/index.tsx +++ b/src/components/TextBoxField/index.tsx @@ -1,9 +1,7 @@ import { TextBox } from '@scaleway/ui' import { FieldState } from 'final-form' import React, { ComponentProps, FocusEvent, Ref, forwardRef } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -64,7 +62,6 @@ export const TextBoxField = forwardRef( beforeSubmit, className, cols, - data, defaultValue, disabled, fillAvailable, @@ -107,33 +104,26 @@ export const TextBoxField = forwardRef( ): JSX.Element => { const { getError } = useErrors() - const validateFn = useValidation({ - validate, - validators: pickValidators({ - max, - maxLength, - min, - minLength, - regex, - required, - }), - }) - const { input, meta } = useField(name, { afterSubmit, allowNull, beforeSubmit, - data, defaultValue, format, formatOnBlur, initialValue, isEqual, + max, + maxLength, + min, + minLength, multiple, parse, + regex, + required, subscription, type, - validate: validateFn, + validate, validateFields, value, }) diff --git a/src/components/TimeField/index.tsx b/src/components/TimeField/index.tsx index 72bf08a78..46bbdf943 100644 --- a/src/components/TimeField/index.tsx +++ b/src/components/TimeField/index.tsx @@ -1,8 +1,6 @@ import { TimeInput } from '@scaleway/ui' import React, { ComponentProps, useMemo } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { BaseFieldProps } from '../../types' const parseTime = (date?: Date | string): { label: string; value: string } => { @@ -47,17 +45,11 @@ export const TimeField = ({ isSearchable, options, }: TimeFieldProps) => { - const validateFn = useValidation({ - validate, - validators: pickValidators({ - required, - }), - }) - const { input, meta } = useField(name, { formatOnBlur, initialValue, - validate: validateFn, + required, + validate, value, }) diff --git a/src/components/ToggleField/index.tsx b/src/components/ToggleField/index.tsx index dc8b3db16..f0957b56a 100644 --- a/src/components/ToggleField/index.tsx +++ b/src/components/ToggleField/index.tsx @@ -1,8 +1,6 @@ import { Toggle } from '@scaleway/ui' import React, { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useField } from '../../hooks' import { BaseFieldProps } from '../../types' type ToggleFieldProps = BaseFieldProps & @@ -46,11 +44,6 @@ export const ToggleField = ({ value, labelPosition, }: ToggleFieldProps) => { - const validateFn = useValidation({ - validate, - validators: pickValidators({ required }), - }) - const { input } = useField(name, { afterSubmit, allowNull, @@ -63,9 +56,10 @@ export const ToggleField = ({ isEqual, multiple, parse, + required, subscription, type: 'checkbox', - validate: validateFn, + validate, validateFields, value, }) diff --git a/src/hooks/__tests__/useField.spec.tsx b/src/hooks/__tests__/useField.spec.tsx new file mode 100644 index 000000000..5b649db1e --- /dev/null +++ b/src/hooks/__tests__/useField.spec.tsx @@ -0,0 +1,15 @@ +import { renderHook } from '@testing-library/react' +import React, { ReactElement } from 'react' +import { Form } from '../../components' +import { mockErrors } from '../../mocks' +import { useField } from '../useField' + +describe('useField', () => { + test('should render correctly', () => { + const wrapper = ({ children }: { children: ReactElement }) => ( +
{children}
+ ) + const { result } = renderHook(() => useField('fieldName', {}), { wrapper }) + expect(result.current).toBeDefined() + }) +}) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d244be2a1..6b054bb12 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export { useValidation } from './useValidation' +export { useField } from './useField' diff --git a/src/hooks/useField.ts b/src/hooks/useField.ts new file mode 100644 index 000000000..3b0391478 --- /dev/null +++ b/src/hooks/useField.ts @@ -0,0 +1,77 @@ +import { useMemo } from 'react' +import { UseFieldConfig, useField as useFinalFormField } from 'react-final-form' +import { pickValidators } from '../helpers' +import { ValidatorProps } from '../types' +import { useValidation } from './useValidation' + +export const useField = < + FieldValue = unknown, + T extends HTMLElement = HTMLElement, + InputValue = FieldValue, +>( + name: string, + { + afterSubmit, + allowNull, + beforeSubmit, + defaultValue, + format, + formatOnBlur, + initialValue, + isEqual, + multiple, + parse, + subscription, + type, + validate, + validateFields, + value, + max, + maxLength, + min, + minLength, + regex, + required, + maxDate, + minDate, + }: UseFieldConfig & ValidatorProps, +) => { + const validators = useMemo( + () => + pickValidators({ + max, + maxDate, + maxLength, + min, + minDate, + minLength, + regex, + required, + }), + [max, maxLength, min, minLength, regex, required, maxDate, minDate], + ) + + const validateFn = useValidation({ validate, validators }) + + // eslint-disable-next-line react-hooks/exhaustive-deps + const data = useMemo(() => ({ key: Math.random() }), [validateFn]) + + return useFinalFormField(name, { + afterSubmit, + allowNull, + beforeSubmit, + data, + defaultValue, + format, + formatOnBlur, + initialValue, + isEqual, + multiple, + parse, + subscription, + type, + validate: validateFn, + validateFields, + value, + }) +} diff --git a/src/hooks/useValidation.ts b/src/hooks/useValidation.ts index 13dd34c03..8de489861 100644 --- a/src/hooks/useValidation.ts +++ b/src/hooks/useValidation.ts @@ -16,8 +16,8 @@ type UseValidationResult = ( export const useValidation = ({ validators, validate, -}: UseValidationParams): UseValidationResult => { - const fn = useCallback( +}: UseValidationParams): UseValidationResult => + useCallback( ( value: T, allValues?: AnyObject, @@ -37,8 +37,5 @@ export const useValidation = ({ return errors.length > 0 ? errors : undefined }, - [validators, validate], + [validate, validators], ) - - return fn -}