From 26a34c06c23f39d70131985f4c44013ae3272679 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 28 Apr 2021 13:08:10 -0400 Subject: [PATCH] fix(useformstate): properly assert that field types inferred from yup schema are defined (#60) Also adds a "Complex field types and validations" example to the docs --- .../useFormState/useFormState.stories.mdx | 22 ++++- .../useFormState/useFormState.stories.tsx | 99 ++++++++++++++++++- src/hooks/useFormState/useFormState.ts | 4 +- tsconfig.json | 2 +- 4 files changed, 120 insertions(+), 7 deletions(-) diff --git a/src/hooks/useFormState/useFormState.stories.mdx b/src/hooks/useFormState/useFormState.stories.mdx index 95297e5..a66d2b9 100644 --- a/src/hooks/useFormState/useFormState.stories.mdx +++ b/src/hooks/useFormState/useFormState.stories.mdx @@ -5,6 +5,7 @@ import { PatternFlyTextFields, PatternFlyTextFieldsWithHelpers, AsyncPrefilling, + ComplexFieldTypes, } from './useFormState.stories.tsx'; import GithubLink from '../../../.storybook/helpers/GithubLink'; @@ -19,8 +20,7 @@ parameters returned by `useFormField` and `useFormState` can be inferred from th passed as individual type parameters to each `useFormField()`, or passed as a unified interface to the type parameter of `useFormState()`. This hook uses [yup](https://github.com/jquense/yup) for field validation, and its TypeScript signature enforces that each of your schema types is -compatible with its field type. `yup` is bundled as a dependency of `@konveyor/lib-ui`, but it is recommended that you also install `@types/yup` as a -devDependency in your app if you are using TypeScript. +compatible with its field type. `yup` is bundled as a dependency of `@konveyor/lib-ui`. We built this solution because we find that most forms do not have complex requirements, and libraries like Formik and co. tend to add unnecessary complexity. We were burned by lots of confusing debugging sessions trying to figure out the reinitialization behavior of Formik, among other things, @@ -115,6 +115,24 @@ Most custom non-text fields can use `getFormGroupProps` even if the other helper +### Complex field types and validations + +You can store any type of value in a form field, but you need to find the right `yup` schema type to use. `yup.string()`, `yup.number()` and `yup.boolean()` are straightforward, +but you may also need to use things like `yup.array()`, `yup.date()`, `yup.object()`. (Note: only use `yup.object` when you need specific validation on each property in the object, which may be better designed as individual useFormField hooks). +If nothing else fits, you can always use `yup.mixed()` which allows you to use any type but does not validate the value for you. If you use `mixed`, be sure to add your own validations with schema methods like `required`, `oneOf`, and `test`. +See the [yup API documentation](https://github.com/jquense/yup#api) for more information. + +This example includes a mixed field and an array field (leveraging the `useSelectionState` hook also provided by lib-ui). It has custom validations to make sure the store you select is open and that you don't select more than 2 items (for some reason). + +Note that the type parameters passed here to `useFormField` are not required, because the field type can usually be inferred from the yup schema. +However, since the schema are non-trivial we're specifying types here explicitly just to be sure the schema match up with what we're expecting. +If you're having trouble with yup schema types resolving to `T | undefined` instead of `T`, try chaining `.default(someDefault)` to tell yup what to validate against if there is an undefined value. +[Learn more about yup's TypeScript support here](https://github.com/jquense/yup/blob/master/docs/typescript.md). + + + + + ### Pre-filling a form asynchronously If the form's initial values are known when it is first rendered, they can simply be passed as the `initialValue` arguments of each `useFormField` call. diff --git a/src/hooks/useFormState/useFormState.stories.tsx b/src/hooks/useFormState/useFormState.stories.tsx index 454b241..5459f22 100644 --- a/src/hooks/useFormState/useFormState.stories.tsx +++ b/src/hooks/useFormState/useFormState.stories.tsx @@ -8,6 +8,7 @@ import { getFormGroupProps, getTextInputProps, getTextAreaProps, + useSelectionState, } from '../..'; export const BasicTextFields: React.FunctionComponent = () => { @@ -29,7 +30,7 @@ export const BasicTextFields: React.FunctionComponent = () => { onBlur={() => form.fields.name.setIsTouched(true)} /> {!form.fields.name.isValid ? ( -

{form.fields.name.error.message}

+

{form.fields.name.error?.message}

) : null}
@@ -42,7 +43,7 @@ export const BasicTextFields: React.FunctionComponent = () => { onBlur={() => form.fields.description.setIsTouched(true)} /> {!form.fields.description.isValid ? ( -

{form.fields.description.error.message}

+

{form.fields.description.error?.message}

) : null}
+ + + ); +}; diff --git a/src/hooks/useFormState/useFormState.ts b/src/hooks/useFormState/useFormState.ts index 64e6fe7..b2c0c87 100644 --- a/src/hooks/useFormState/useFormState.ts +++ b/src/hooks/useFormState/useFormState.ts @@ -52,7 +52,7 @@ export interface IFormState { export const useFormField = ( initialValue: T, - schema: yup.AnySchema, + schema: yup.AnySchema, options: { initialTouched?: boolean } = {} ): IFormField => { const [initializedValue, setInitializedValue] = React.useState(initialValue); @@ -72,7 +72,7 @@ export const useFormField = ( setValue(initializedValue); setIsTouched(options.initialTouched || false); }, - schema, + schema: schema.defined(), }; }; diff --git a/tsconfig.json b/tsconfig.json index 84cb785..b55c990 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,5 +20,5 @@ "skipLibCheck": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/**/*.stories.tsx", "src/**/*.test.tsx"] + "exclude": ["node_modules", "dist"] }