From 35873b189781a70357587a0f31736ca04e45a052 Mon Sep 17 00:00:00 2001 From: Matt Hardman Date: Fri, 16 Sep 2022 11:48:45 +0100 Subject: [PATCH] console: create new form component without using forwardRef PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5755 GitOrigin-RevId: 9078553499377484b15d53bfbe0c19eaee9a376a --- console/src/new-components/Form/Form.tsx | 55 +++ .../src/new-components/Form/InputField.tsx | 90 ++-- .../Form/UpdatedForm.stories.tsx | 445 ++++++++++++++++++ .../Form/__tests__/InputField.spec.tsx | 6 +- 4 files changed, 551 insertions(+), 45 deletions(-) create mode 100644 console/src/new-components/Form/UpdatedForm.stories.tsx diff --git a/console/src/new-components/Form/Form.tsx b/console/src/new-components/Form/Form.tsx index 4f314f654ad..77603a5b3a0 100644 --- a/console/src/new-components/Form/Form.tsx +++ b/console/src/new-components/Form/Form.tsx @@ -82,3 +82,58 @@ export const Form = React.forwardRef( ); } ); + +type TFormValues = Record; + +type Schema = ZodType; + +export const UpdatedForm = ( + props: FormProps, FormSchema> & { + autoFocus?: Path>; + trigger?: boolean; + } +) => { + const { + id, + options, + schema, + onSubmit, + className, + children, + autoFocus, + trigger, + ...rest + } = props; + + const methods = useForm>({ + ...options, + resolver: schema && zodResolver(schema), + }); + + React.useEffect(() => { + if (autoFocus) { + methods.setFocus(autoFocus); + } + if (trigger) { + methods.trigger(); + } + }, [trigger, autoFocus, methods]); + + return ( + +
+ {children(methods)} +
+
+ ); +}; + +UpdatedForm.defaultProps = { + autoFocus: undefined, + trigger: false, +}; diff --git a/console/src/new-components/Form/InputField.tsx b/console/src/new-components/Form/InputField.tsx index 25856d3f56c..f1b53be5a0f 100644 --- a/console/src/new-components/Form/InputField.tsx +++ b/console/src/new-components/Form/InputField.tsx @@ -1,49 +1,55 @@ import React, { ReactElement } from 'react'; import clsx from 'clsx'; import get from 'lodash.get'; -import { FieldError, useFormContext } from 'react-hook-form'; +import { FieldError, FieldPath, useFormContext } from 'react-hook-form'; +import { z, ZodTypeDef, ZodType } from 'zod'; import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper'; -export type InputFieldProps = FieldWrapperPassThroughProps & { - /** - * The input field name - */ - name: string; - /** - * The input field type - */ - type?: 'text' | 'email' | 'password' | 'number'; - /** - * The input field classes - */ - className?: string; - /** - * The input field icon - */ - icon?: ReactElement; - /** - * The input field icon position - */ - iconPosition?: 'start' | 'end'; - /** - * The input field placeholder - */ - placeholder?: string; - /** - * Flag to indicate if the field is disabled - */ - disabled?: boolean; - /** - * The input field prepend label - */ - prependLabel?: string; - /** - * The input field append label - */ - appendLabel?: string; -}; +type TFormValues = Record; -export const InputField: React.FC = ({ +type Schema = ZodType; + +export type InputFieldProps> = + FieldWrapperPassThroughProps & { + /** + * The input field name + */ + name: FieldPath; + /** + * The input field type + */ + type?: 'text' | 'email' | 'password' | 'number'; + /** + * The input field classes + */ + className?: string; + /** + * The input field icon + */ + icon?: ReactElement; + /** + * The input field icon position + */ + iconPosition?: 'start' | 'end'; + /** + * The input field placeholder + */ + placeholder?: string; + /** + * Flag to indicate if the field is disabled + */ + disabled?: boolean; + /** + * The input field prepend label + */ + prependLabel?: string; + /** + * The input field append label + */ + appendLabel?: string; + }; + +export const InputField = >({ type = 'text', name, icon, @@ -54,11 +60,11 @@ export const InputField: React.FC = ({ appendLabel = '', dataTest, ...wrapperProps -}: InputFieldProps) => { +}: InputFieldProps) => { const { register, formState: { errors }, - } = useFormContext(); + } = useFormContext(); const maybeError = get(errors, name) as FieldError | undefined; return ( diff --git a/console/src/new-components/Form/UpdatedForm.stories.tsx b/console/src/new-components/Form/UpdatedForm.stories.tsx new file mode 100644 index 00000000000..147ee409f4a --- /dev/null +++ b/console/src/new-components/Form/UpdatedForm.stories.tsx @@ -0,0 +1,445 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import { DevTool } from '@hookform/devtools'; +import { z } from 'zod'; +import { + UpdatedForm as Form, + InputField, + Textarea, + Select, + Checkbox, + Radio, + CodeEditorField, +} from '@/new-components/Form'; +import { Button } from '@/new-components/Button'; + +export default { + title: 'components/Forms πŸ“/Updated Form🦠', + component: Form, + parameters: { + docs: { + description: { + component: ``, + }, + source: { type: 'code', state: 'open' }, + }, + }, +} as ComponentMeta; + +export const Primary = () => { + const validationSchema = z.object({ + inputFieldName: z.string().min(1, { message: 'Mandatory field' }), + }); + const onSubmit = (output: z.infer) => { + action('onSubmit')(output); + }; + + type Schema = z.infer; + + return ( +
+ {/* πŸ’‘ The `control` prop is provided by [**React Hook Form**](https://react-hook-form.com/api/useform) +and is used to access the form state. */} + {({ control }) => ( +
+

Basic form

+ + name="inputFieldName" + label="The input field label" + placeholder="Input field placeholder" + /> + + {/* Debug form state */} + +
+ )} +
+ ); +}; + +export const FormInputDefaultValue: ComponentStory = () => { + const validationSchema = z.object({ + inputFieldName: z.string().min(1, { message: 'Mandatory field' }), + }); + + const onSubmit = (output: z.infer) => { + action('onSubmit')(output); + }; + + return ( +
+ {({ control }) => ( +
+

Default value

+ + + +
+ )} +
+ ); +}; +FormInputDefaultValue.storyName = 'πŸ’  Form input default value'; +FormInputDefaultValue.parameters = { + docs: { + description: { + story: `In this example, the form is automatically filled with the \`Hello world !\` + value for the \`inputFieldName\` input.`, + }, + }, +}; + +export const ManuallyTriggerFormValidation: ComponentStory = + () => { + const validationSchema = z.object({ + inputFieldName: z.string().min(1, { message: 'Mandatory field' }), + }); + + return ( +
+ {options => { + return ( +
+

+ Manually trigger form validation +

+ + + {/* Debug form state */} + +
+ ); + }} +
+ ); + }; +ManuallyTriggerFormValidation.storyName = 'πŸ’  Manually trigger form validation'; +ManuallyTriggerFormValidation.parameters = { + docs: { + description: { + story: `In this example, the form is automatically validated thanks to a \`useImperativeHandle\` hook.`, + }, + }, +}; + +export const ManuallyFocusField: ComponentStory = () => { + const validationSchema = z.object({ + codeEditorFieldName: z.string().min(1, { message: 'Mandatory field' }), + }); + + return ( +
+ {({ control }) => ( +
+

Manually focus field

+ + + {/* Debug form state */} + +
+ )} +
+ ); +}; +ManuallyFocusField.storyName = 'πŸ’  Manually focus a field'; +ManuallyFocusField.parameters = { + docs: { + description: { + story: `In this example, the form \`codeEditorFieldName\` field is automatically focused thanks to a \`useImperativeHandle\` hook.`, + }, + }, +}; + +export const AllInputs: ComponentStory = () => { + const validationSchema = z.object({ + inputFieldName: z.string().min(1, { message: 'Mandatory field' }), + textareaName: z.string().min(1, { message: 'Mandatory field' }), + selectName: z.string().min(1, { message: 'Mandatory field' }), + checkboxNames: z + // When nothing is selected, the value is a false boolean + .union([z.string().array(), z.boolean()]) + .refine( + value => Array.isArray(value) && value.length > 0, + 'Choose at least one option' + ), + radioName: z + // When nothing is selected, the value is null + .union([z.string(), z.null()]) + .refine( + value => typeof value === 'string' && value.length > 0, + 'Choose one option' + ), + codeEditorFieldName: z.string().min(1, { message: 'Mandatory field' }), + }); + + return ( +
+ {({ control, reset }) => ( +
+

Form title

+ +