console: create new form component without using forwardRef

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5755
GitOrigin-RevId: 9078553499377484b15d53bfbe0c19eaee9a376a
This commit is contained in:
Matt Hardman 2022-09-16 11:48:45 +01:00 committed by hasura-bot
parent bd071ddc7f
commit 35873b1897
4 changed files with 551 additions and 45 deletions

View File

@ -82,3 +82,58 @@ export const Form = React.forwardRef(
);
}
);
type TFormValues = Record<string, unknown>;
type Schema = ZodType<TFormValues, ZodTypeDef, TFormValues>;
export const UpdatedForm = <FormSchema extends Schema>(
props: FormProps<zodInfer<FormSchema>, FormSchema> & {
autoFocus?: Path<zodInfer<FormSchema>>;
trigger?: boolean;
}
) => {
const {
id,
options,
schema,
onSubmit,
className,
children,
autoFocus,
trigger,
...rest
} = props;
const methods = useForm<zodInfer<FormSchema>>({
...options,
resolver: schema && zodResolver(schema),
});
React.useEffect(() => {
if (autoFocus) {
methods.setFocus(autoFocus);
}
if (trigger) {
methods.trigger();
}
}, [trigger, autoFocus, methods]);
return (
<FormProvider {...methods}>
<form
id={id}
className={`space-y-md bg-legacybg p-4 ${className || ''}`}
onSubmit={methods.handleSubmit(onSubmit)}
{...rest}
>
{children(methods)}
</form>
</FormProvider>
);
};
UpdatedForm.defaultProps = {
autoFocus: undefined,
trigger: false,
};

View File

@ -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<string, unknown>;
export const InputField: React.FC<InputFieldProps> = ({
type Schema = ZodType<TFormValues, ZodTypeDef, TFormValues>;
export type InputFieldProps<T extends z.infer<Schema>> =
FieldWrapperPassThroughProps & {
/**
* The input field name
*/
name: FieldPath<T>;
/**
* 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 = <T extends z.infer<Schema>>({
type = 'text',
name,
icon,
@ -54,11 +60,11 @@ export const InputField: React.FC<InputFieldProps> = ({
appendLabel = '',
dataTest,
...wrapperProps
}: InputFieldProps) => {
}: InputFieldProps<T>) => {
const {
register,
formState: { errors },
} = useFormContext();
} = useFormContext<T>();
const maybeError = get(errors, name) as FieldError | undefined;
return (

View File

@ -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<typeof Form>;
export const Primary = () => {
const validationSchema = z.object({
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
});
const onSubmit = (output: z.infer<typeof validationSchema>) => {
action('onSubmit')(output);
};
type Schema = z.infer<typeof validationSchema>;
return (
<Form
// Apply validation schema to the form
schema={validationSchema}
onSubmit={onSubmit}
>
{/* 💡 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 }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Basic form</h1>
<InputField<Schema>
name="inputFieldName"
label="The input field label"
placeholder="Input field placeholder"
/>
<Button type="submit" mode="primary">
Submit
</Button>
{/* Debug form state */}
<DevTool control={control} />
</div>
)}
</Form>
);
};
export const FormInputDefaultValue: ComponentStory<typeof Form> = () => {
const validationSchema = z.object({
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
});
const onSubmit = (output: z.infer<typeof validationSchema>) => {
action('onSubmit')(output);
};
return (
<Form
schema={validationSchema}
options={{
defaultValues: {
inputFieldName: 'Hello world !',
},
}}
onSubmit={onSubmit}
>
{({ control }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Default value</h1>
<InputField
name="inputFieldName"
label="The input field label"
placeholder="Input field placeholder"
/>
<Button type="submit" mode="primary">
Submit
</Button>
<DevTool control={control} />
</div>
)}
</Form>
);
};
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<typeof Form> =
() => {
const validationSchema = z.object({
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
});
return (
<Form
id="formId"
schema={validationSchema}
trigger
onSubmit={action('onSubmit')}
>
{options => {
return (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">
Manually trigger form validation
</h1>
<InputField
name="inputFieldName"
label="The input field label"
placeholder="Input field placeholder"
/>
<Button type="submit" mode="primary">
Submit
</Button>
{/* Debug form state */}
<DevTool control={options.control} />
</div>
);
}}
</Form>
);
};
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<typeof Form> = () => {
const validationSchema = z.object({
codeEditorFieldName: z.string().min(1, { message: 'Mandatory field' }),
});
return (
<Form
id="formId"
schema={validationSchema}
autoFocus="codeEditorFieldName"
onSubmit={action('onSubmit')}
>
{({ control }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Manually focus field</h1>
<CodeEditorField
name="codeEditorFieldName"
label="The code editor field label"
/>
<Button type="submit" mode="primary">
Submit
</Button>
{/* Debug form state */}
<DevTool control={control} />
</div>
)}
</Form>
);
};
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<typeof Form> = () => {
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 (
<Form
id="formId"
options={{
mode: 'onSubmit',
reValidateMode: 'onChange',
}}
schema={validationSchema}
onSubmit={action('onSubmit')}
>
{({ control, reset }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Form title</h1>
<InputField
name="inputFieldName"
label="The input field label"
description="The input field description"
tooltip="The input field tooltip"
placeholder="Input field placeholder"
/>
<Textarea
name="textareaName"
label="The textarea label"
description="The textarea description"
tooltip="The textarea tooltip"
placeholder="Textarea field placeholder"
/>
<Select
name="selectName"
options={[
{ value: 'selectValue0', label: 'Select value 0' },
{
value: 'selectValue1',
label: 'Select value 1',
disabled: true,
},
{ value: 'selectValue2', label: 'Select value 2' },
]}
label="The select label *"
description="The select description"
tooltip="The select tooltip"
placeholder="--Select placeholder--"
/>
<Checkbox
name="checkboxNames"
label="The checkbox label *"
description="The checkbox description"
tooltip="The checkbox tooltip"
options={[
{ value: 'checkboxValue0', label: 'Checkbox value 0' },
{
value: 'checkboxValue1',
label: 'Checkbox value 1',
disabled: true,
},
{ value: 'checkboxValue2', label: 'Checkbox value 2' },
]}
orientation="horizontal"
/>
<Radio
name="radioName"
label="The radio label *"
description="The radio description"
tooltip="The radio tooltip"
options={[
{ value: 'radioValue0', label: 'Radio value 0' },
{
value: 'radioValue1',
label: 'Radio value 1',
disabled: true,
},
{ value: 'radioValue2', label: 'Radio value 2' },
]}
orientation="horizontal"
/>
<CodeEditorField
name="codeEditorFieldName"
label="The code editor label *"
description="The code editor description"
tooltip="The code editor tooltip"
/>
<div className="flex gap-4">
<Button type="button" onClick={() => reset({})}>
Reset
</Button>
<Button type="submit" mode="primary">
Submit
</Button>
</div>
<DevTool control={control} />
</div>
)}
</Form>
);
};
AllInputs.storyName = '💠 Demo with all inputs and form reset';
AllInputs.parameters = {
docs: {
description: {
story: `Validation schema with all inputs mandatory.`,
},
},
};
export const AllInputsHorizontal: ComponentStory<typeof Form> = () => {
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 (
<Form
id="formId"
options={{
mode: 'onSubmit',
reValidateMode: 'onChange',
}}
schema={validationSchema}
onSubmit={action('onSubmit')}
>
{({ control, reset }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Form title</h1>
<InputField
name="inputFieldName"
label="The input field label"
description="The input field description"
tooltip="The input field tooltip"
placeholder="Input field placeholder"
horizontal
/>
<Textarea
name="textareaName"
label="The textarea label"
description="The textarea description"
tooltip="The textarea tooltip"
placeholder="Textarea field placeholder"
horizontal
/>
<Select
name="selectName"
options={[
{ value: 'selectValue0', label: 'Select value 0' },
{
value: 'selectValue1',
label: 'Select value 1',
disabled: true,
},
{ value: 'selectValue2', label: 'Select value 2' },
]}
label="The select label *"
description="The select description"
tooltip="The select tooltip"
placeholder="--Select placeholder--"
horizontal
/>
<Checkbox
name="checkboxNames"
label="The checkbox label *"
description="The checkbox description"
tooltip="The checkbox tooltip"
options={[
{ value: 'checkboxValue0', label: 'Checkbox value 0' },
{
value: 'checkboxValue1',
label: 'Checkbox value 1',
disabled: true,
},
{ value: 'checkboxValue2', label: 'Checkbox value 2' },
]}
orientation="vertical"
horizontal
/>
<Radio
name="radioName"
label="The radio label *"
description="The radio description"
tooltip="The radio tooltip"
options={[
{ value: 'radioValue0', label: 'Radio value 0' },
{
value: 'radioValue1',
label: 'Radio value 1',
disabled: true,
},
{ value: 'radioValue2', label: 'Radio value 2' },
]}
orientation="vertical"
horizontal
/>
<CodeEditorField
name="codeEditorFieldName"
label="The code editor label *"
description="The code editor description"
tooltip="The code editor tooltip"
horizontal
/>
<div className="flex gap-4">
<Button type="button" onClick={() => reset({})}>
Reset
</Button>
<Button type="submit" mode="primary">
Submit
</Button>
</div>
<DevTool control={control} />
</div>
)}
</Form>
);
};
AllInputsHorizontal.storyName = ' 💠 Demo with horizontal fields';

View File

@ -1,11 +1,11 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { z } from 'zod';
import { Form, InputField, InputFieldProps } from '@/new-components/Form';
import { Button } from '@/new-components/Button';
import { Form, InputField, InputFieldProps } from '..';
import { Button } from '../../Button';
const renderInputField = (
props: Omit<InputFieldProps, 'name'>,
props: Omit<InputFieldProps<any>, 'name'>,
schema: any = z.object({ title: z.string() })
) => {
const onSubmit = jest.fn();