From aac01bd03d5cf715d972191793a9d5438fbbbcbe Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 7 Oct 2022 10:57:46 +0200 Subject: [PATCH] feat: enable dynamic validation (#510) * feat: enable dynamic validation * refactor: rename custom useField to useFormField --- src/components/CheckboxField/index.tsx | 16 +--- src/components/DateField/index.tsx | 19 ++--- src/components/RadioField/index.tsx | 16 +--- src/components/RichSelectField/index.tsx | 17 ++-- src/components/SelectNumberField/index.tsx | 16 +--- src/components/SelectableCardField/index.tsx | 16 +--- src/components/TagsField/index.tsx | 16 +--- .../__stories__/index.stories.tsx | 28 ++++++- src/components/TextBoxField/index.tsx | 28 +++---- src/components/TimeField/index.tsx | 16 +--- src/components/ToggleField/index.tsx | 14 +--- src/hooks/__tests__/useFormField.spec.tsx | 17 ++++ src/hooks/index.ts | 1 + src/hooks/useFormField.ts | 77 +++++++++++++++++++ src/hooks/useValidation.ts | 9 +-- 15 files changed, 173 insertions(+), 133 deletions(-) create mode 100644 src/hooks/__tests__/useFormField.spec.tsx create mode 100644 src/hooks/useFormField.ts diff --git a/src/components/CheckboxField/index.tsx b/src/components/CheckboxField/index.tsx index a5f6fda3..c150f518 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 { ComponentProps, ReactNode, Ref, forwardRef } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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, { + const { input, meta } = useFormField(name, { + required, type: 'checkbox', - validate: validateFn, + validate, value, }) diff --git a/src/components/DateField/index.tsx b/src/components/DateField/index.tsx index 98db6495..e7d2d140 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 { ComponentProps, FocusEvent } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -58,19 +56,14 @@ export const DateField = ({ autoFocus = false, }: DateFieldProps) => { const { getError } = useErrors() - const validateFn = useValidation({ - validate, - validators: pickValidators({ - maxDate, - minDate, - required, - }), - }) - const { input, meta } = useField(name, { + const { input, meta } = useFormField(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 83b60082..bffff346 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 { ComponentProps, ReactNode } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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, { + const { input, meta } = useFormField(name, { + required, type: 'radio', - validate: validateFn, + validate, value, }) diff --git a/src/components/RichSelectField/index.tsx b/src/components/RichSelectField/index.tsx index 9c6e23be..e6b72b84 100644 --- a/src/components/RichSelectField/index.tsx +++ b/src/components/RichSelectField/index.tsx @@ -7,9 +7,7 @@ import { useCallback, useMemo, } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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( () => @@ -164,12 +155,14 @@ export const RichSelectField = < [formatProp, multiple, name, options], ) - const { input, meta } = useField(name, { + const { input, meta } = useFormField(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 245d789c..9fa5a673 100644 --- a/src/components/SelectNumberField/index.tsx +++ b/src/components/SelectNumberField/index.tsx @@ -1,8 +1,6 @@ import { SelectNumber } from '@scaleway/ui' import { ComponentProps, FocusEvent, FocusEventHandler } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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, { + const { input } = useFormField(name, { + required, type: 'number', - validate: validateFn, + validate, value, }) diff --git a/src/components/SelectableCardField/index.tsx b/src/components/SelectableCardField/index.tsx index e1e64057..c15281e4 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 { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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, { + const { input, meta } = useFormField(name, { + required, type: type ?? 'radio', - validate: validateFn, + validate, value, }) diff --git a/src/components/TagsField/index.tsx b/src/components/TagsField/index.tsx index 1985845a..63b036b8 100644 --- a/src/components/TagsField/index.tsx +++ b/src/components/TagsField/index.tsx @@ -1,8 +1,6 @@ import { Tags } from '@scaleway/ui' import { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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, { + const { input } = useFormField(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 55ed99b2..c3be1edf 100644 --- a/src/components/TextBoxField/__stories__/index.stories.tsx +++ b/src/components/TextBoxField/__stories__/index.stories.tsx @@ -1,5 +1,6 @@ +import { Checkbox } from '@scaleway/ui' import { Meta, Story } from '@storybook/react' -import { ComponentProps } from 'react' +import { ComponentProps, useState } from 'react' import { Form, Submit, TextBoxField } from '../..' import { mockErrors } from '../../../mocks/mockErrors' @@ -53,6 +54,31 @@ Required.args = { required: true, } +export const DynamicRequired: Story< + ComponentProps +> = args => { + const [isRequired, setIsRequired] = 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 4704a18e..95e57d14 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 { ComponentProps, FocusEvent, Ref, forwardRef } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } from '../../hooks' import { useErrors } from '../../providers/ErrorContext' import { BaseFieldProps } from '../../types' @@ -66,7 +64,6 @@ export const TextBoxField = forwardRef( beforeSubmit, className, cols, - data, defaultValue, disabled, fillAvailable, @@ -111,33 +108,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, { + const { input, meta } = useFormField(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 52c9b49b..237fa211 100644 --- a/src/components/TimeField/index.tsx +++ b/src/components/TimeField/index.tsx @@ -1,8 +1,6 @@ import { TimeInput } from '@scaleway/ui' import { ComponentProps, useMemo } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } 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, { + const { input, meta } = useFormField(name, { formatOnBlur, initialValue, - validate: validateFn, + required, + validate, value, }) diff --git a/src/components/ToggleField/index.tsx b/src/components/ToggleField/index.tsx index 99fe2c7d..b1a6ea9d 100644 --- a/src/components/ToggleField/index.tsx +++ b/src/components/ToggleField/index.tsx @@ -1,8 +1,6 @@ import { Toggle } from '@scaleway/ui' import { ComponentProps } from 'react' -import { useField } from 'react-final-form' -import { pickValidators } from '../../helpers' -import { useValidation } from '../../hooks' +import { useFormField } from '../../hooks' import { BaseFieldProps } from '../../types' type ToggleFieldProps = BaseFieldProps & @@ -46,12 +44,7 @@ export const ToggleField = ({ value, labelPosition, }: ToggleFieldProps) => { - const validateFn = useValidation({ - validate, - validators: pickValidators({ required }), - }) - - const { input } = useField(name, { + const { input } = useFormField(name, { afterSubmit, allowNull, beforeSubmit, @@ -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__/useFormField.spec.tsx b/src/hooks/__tests__/useFormField.spec.tsx new file mode 100644 index 00000000..8de69451 --- /dev/null +++ b/src/hooks/__tests__/useFormField.spec.tsx @@ -0,0 +1,17 @@ +import { renderHook } from '@testing-library/react' +import { ReactElement } from 'react' +import { Form } from '../../components' +import { mockErrors } from '../../mocks' +import { useFormField } from '../useFormField' + +describe('useFormField', () => { + test('should render correctly', () => { + const wrapper = ({ children }: { children: ReactElement }) => ( +
{children}
+ ) + const { result } = renderHook(() => useFormField('fieldName', {}), { + wrapper, + }) + expect(result.current).toBeDefined() + }) +}) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d244be2a..d28fd5f0 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export { useValidation } from './useValidation' +export { useFormField } from './useFormField' diff --git a/src/hooks/useFormField.ts b/src/hooks/useFormField.ts new file mode 100644 index 00000000..1ece5099 --- /dev/null +++ b/src/hooks/useFormField.ts @@ -0,0 +1,77 @@ +import { useMemo } from 'react' +import { UseFieldConfig, useField } from 'react-final-form' +import { pickValidators } from '../helpers' +import { ValidatorProps } from '../types' +import { useValidation } from './useValidation' + +export const useFormField = < + 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 useField(name, { + afterSubmit, + allowNull, + beforeSubmit, + defaultValue, + format, + formatOnBlur, + initialValue, + isEqual, + multiple, + parse, + subscription, + type, + validate: validateFn, + validateFields, + value, + data, + }) +} diff --git a/src/hooks/useValidation.ts b/src/hooks/useValidation.ts index 13dd34c0..8de48986 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 -}