From ce95b0cd7ecbe2290d4007c4eb0d9ad70ec60662 Mon Sep 17 00:00:00 2001 From: Steven Than Date: Thu, 17 Nov 2022 12:10:53 -0800 Subject: [PATCH] chore: wip --- .eslintrc.json | 1 + package.json | 2 +- src/components/Form/FormRow.tsx | 2 +- src/components/HookForm/Form.stories.tsx | 202 ++++++++++++++------- src/components/HookForm/Form.tsx | 57 +++++- src/components/HookForm/FormFeedback.tsx | 71 ++++++++ src/components/HookForm/FormLabelGroup.tsx | 55 ++++++ src/components/HookForm/FormRow.tsx | 51 ++++++ src/components/HookForm/Input.tsx | 8 +- src/components/HookForm/index.tsx | 7 +- src/components/HookForm/types.tsx | 8 + 11 files changed, 383 insertions(+), 81 deletions(-) create mode 100644 src/components/HookForm/FormFeedback.tsx create mode 100644 src/components/HookForm/FormLabelGroup.tsx create mode 100644 src/components/HookForm/FormRow.tsx create mode 100644 src/components/HookForm/types.tsx diff --git a/.eslintrc.json b/.eslintrc.json index 2d7b80193..59d12b8e2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,7 @@ "extends": ["@appfolio/eslint-config-appfolio-react", "prettier"], "plugins": ["no-only-tests", "react-hooks", "@typescript-eslint"], "rules": { + "camelcase": ["error", { "allow": "^experimental_" }], "curly": ["error", "all"], "react/jsx-props-no-spreading": "off", "react/static-property-placement": "off", diff --git a/package.json b/package.json index 5d0b3b2af..f276a2489 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@appfolio/react-gears", - "version": "7.9.0", + "version": "7.10.0-hook-form.8", "description": "React-based version of Gears", "author": "Appfolio, Inc.", "repository": { diff --git a/src/components/Form/FormRow.tsx b/src/components/Form/FormRow.tsx index 9678def33..bc4cc398b 100644 --- a/src/components/Form/FormRow.tsx +++ b/src/components/Form/FormRow.tsx @@ -17,7 +17,7 @@ const gearsInputs = { type PropOrDefault = K extends keyof T ? T[K] : D; type ReactStrapInputTypes = NonNullable['type']>; -type InputTypes = ReactStrapInputTypes | keyof typeof gearsInputs; +export type InputTypes = ReactStrapInputTypes | keyof typeof gearsInputs; function getInputByType(type: T) { return ( diff --git a/src/components/HookForm/Form.stories.tsx b/src/components/HookForm/Form.stories.tsx index 80019f69e..7fd668f1b 100755 --- a/src/components/HookForm/Form.stories.tsx +++ b/src/components/HookForm/Form.stories.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { SubmitHandler } from 'react-hook-form'; -import { FormFeedback } from 'reactstrap'; import Button from '../Button/Button'; import FormGroup from '../Form/FormGroup'; import Label from '../Label/Label'; import Form from './Form'; +import FormFeedback from './FormFeedback'; +import FormLabelGroup from './FormLabelGroup'; +import FormRow from './FormRow'; import Input from './Input'; +import { SubmitHandler } from './types'; export default { title: 'react-hook-form', @@ -15,86 +17,158 @@ interface FormInputs { email: string; age: string; select: string; + address: { + line1: string; + line2: string; + state: string; + zipCode: string; + }; } -export const LiveExample = () => { - const handleSubmit: SubmitHandler = (formData) => { +export const FormWithValidations = () => { + const handleSubmit: SubmitHandler = (formData, { setError }) => { + // make api calss + // if fails then call setError + setError('address.line1', { + message: 'something went wrong with line1', + }); console.log(formData); }; return (
- {({ formState: { errors, dirtyFields } }) => ( + {({ reset, formState: { errors, dirtyFields } }) => { + console.log('render'); + console.log(errors); + return ( + <> +
+ + + + + + +
+
+ Address + + + + + + + + + +
+
+ + + {errors.age?.message} +
+
+ + + + + + + + + {errors.select?.message} +
+
+ + + + + + + + + +
+
+ + + + +
+
+ Radio Buttons + + {' '} + + + + {' '} + + +
+ + + + ); + }} +
+ ); +}; + +interface FormValues { + email: string; +} + +export const SimpleFormNoValidation = () => { + const handleSubmit: SubmitHandler = (formData) => { + console.log(formData); + }; + + return ( +
+ {({ formState: { isValid } }) => ( <> -
- - value === 'email' || 'incorrect'} - /> - {errors.email?.message} - Looks good! -
-
- - - {errors.age?.message} -
-
- - - - - - - - - {errors.select?.message} -
-
- Radio Buttons - - {' '} - - - - {' '} - - -
- + + )} ); }; -export const TestExample = () => { - const handleSubmit: SubmitHandler = (formData) => { +export const FormDemo = () => { + const handleSubmit: SubmitHandler = (formData, { reset }) => { console.log(formData); + reset(); }; return ( -
-
- - - Looks good! -
+ + + + + + + +
); }; diff --git a/src/components/HookForm/Form.tsx b/src/components/HookForm/Form.tsx index a0086ed11..412ace896 100644 --- a/src/components/HookForm/Form.tsx +++ b/src/components/HookForm/Form.tsx @@ -1,29 +1,68 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, ComponentProps } from 'react'; import { useForm, FormProvider, - SubmitHandler, + SubmitHandler as HookFormSubmitHandler, UseFormProps, UseFormReturn, FieldValues, } from 'react-hook-form'; import GearsForm from '../Form/Form'; +import { SubmitHandler } from './types'; -interface FormProps extends UseFormProps { +type BaseFormProps = { onSubmit: SubmitHandler; children: ((useFormReturn: UseFormReturn) => ReactNode) | ReactNode; -} +}; + +type FormProps = Omit< + ComponentProps, + keyof BaseFormProps +> & + UseFormProps & + BaseFormProps; -const Form = ({ +const Form = ({ children, - onSubmit, - ...useFormProps + action, + method, + onSubmit = () => undefined, + mode, + reValidateMode, + defaultValues, + resolver, + context, + criteriaMode, + shouldFocusError, + shouldUnregister, + shouldUseNativeValidation, + delayError, + ...gearsFormProps }: FormProps) => { - const useFormReturn = useForm(useFormProps); + const useFormReturn = useForm({ + mode, + reValidateMode, + defaultValues, + resolver, + context, + criteriaMode, + shouldFocusError, + shouldUnregister, + shouldUseNativeValidation, + delayError, + }); + + const handleFormSubmit: HookFormSubmitHandler = async (formData, event) => { + return await onSubmit(formData, { ...useFormReturn }, event); + }; return ( - + {typeof children === 'function' ? children(useFormReturn) : children} diff --git a/src/components/HookForm/FormFeedback.tsx b/src/components/HookForm/FormFeedback.tsx new file mode 100644 index 000000000..d52d9e6b2 --- /dev/null +++ b/src/components/HookForm/FormFeedback.tsx @@ -0,0 +1,71 @@ +import React, { + ComponentProps, + ReactNode, + Children, + cloneElement, + isValidElement, + ReactElement, +} from 'react'; +import { useFormState } from 'react-hook-form'; +import GearsFormFeedback from '../Form/FormFeedback'; +import Input, { InputProps } from './Input'; + +export type InputChildProps = { + id?: string; + name: string; +}; + +type BaseFormFeedbackProps = { + name?: string; +}; + +type FormFeedbackProps = Omit< + ComponentProps, + keyof BaseFormFeedbackProps +> & + BaseFormFeedbackProps; + +const isValidChildWithNameProp = (child: ReactNode): child is ReactElement => + isValidElement(child) && typeof child.props.name === 'string'; + +export const findChildWithNameProp = (children: ReactNode) => { + return Children.toArray(children).find(isValidChildWithNameProp); +}; + +const FormFeedback = ({ name, children, ...gearsFormFeedbackProps }: FormFeedbackProps) => { + const { errors } = useFormState(); + + if (name) { + const error = errors[name]; + return ( + <> + {error && ( + + {error.message} + + )} + + ); + } + + const childWithNameProp = findChildWithNameProp(children); + if (!childWithNameProp) { + return {children}; + } + + const error = errors[childWithNameProp.props.name]; + + return ( + <> + {Children.map(children, (child) => { + if (isValidElement(child) && child.type === Input) { + return cloneElement(child as ReactElement, { invalid: !!error }); + } + return child; + })} + {error && {error.message}} + + ); +}; + +export default FormFeedback; diff --git a/src/components/HookForm/FormLabelGroup.tsx b/src/components/HookForm/FormLabelGroup.tsx new file mode 100644 index 000000000..5b250b538 --- /dev/null +++ b/src/components/HookForm/FormLabelGroup.tsx @@ -0,0 +1,55 @@ +import React, { + ComponentProps, + ReactNode, + Children, + cloneElement, + isValidElement, + ReactElement, +} from 'react'; +import { useFormState } from 'react-hook-form'; +import GearsFormLabelGroup from '../Form/FormLabelGroup'; +import { findChildWithNameProp } from './FormFeedback'; +import Input, { InputProps } from './Input'; + +type FormLabelGroupProps = ComponentProps< + typeof GearsFormLabelGroup +>; + +const FormLabelGroup = ({ + children, + ...gearsFormLabelGroupProps +}: FormLabelGroupProps) => { + const { errors } = useFormState(); + const childWithNameProp = findChildWithNameProp(children); + + if (!childWithNameProp) { + return ( + + {children} + + ); + } + + const { name, id } = childWithNameProp.props; + const inputError = errors[name]; + + return ( + + {Children.map(children, (child) => { + if (isValidElement(child) && child.type === Input) { + return cloneElement( + child as ReactElement, + { invalid: !!inputError } + ); + } + return child; + })} + + ); +}; + +export default FormLabelGroup; diff --git a/src/components/HookForm/FormRow.tsx b/src/components/HookForm/FormRow.tsx new file mode 100644 index 000000000..717f1527e --- /dev/null +++ b/src/components/HookForm/FormRow.tsx @@ -0,0 +1,51 @@ +import React, { ComponentProps, useCallback } from 'react'; +import { useController, UseControllerProps } from 'react-hook-form'; +import GearsFormRow, { InputTypes } from '../Form/FormRow'; + +type BaseFormRowProps = { + // showInvalidFeedback?: boolean; + // showValidFeedback?: boolean; +}; + +type ExcludedGearsFormRowProps = 'feedback' | 'validFeedback'; + +type FormRowProps = Omit< + ComponentProps>, + keyof BaseFormRowProps | ExcludedGearsFormRowProps +> & + Omit & + BaseFormRowProps; + +const FormRow = ({ + defaultValue, + name, + onChange: formRowOnChange, + rules, + ...gearsFormRowProps +}: FormRowProps) => { + const { + field: { onChange, onBlur }, + fieldState: { error }, + } = useController({ name, defaultValue, rules }); + + const handleChange = useCallback( + (e: any) => { + console.log(e); + onChange(e); + formRowOnChange?.(e); + }, + [onChange, formRowOnChange] + ); + + return ( + + ); +}; + +export default FormRow; diff --git a/src/components/HookForm/Input.tsx b/src/components/HookForm/Input.tsx index f64b6abe9..7988b9be5 100644 --- a/src/components/HookForm/Input.tsx +++ b/src/components/HookForm/Input.tsx @@ -10,10 +10,10 @@ type DetermineValidateValue< TValueAsDate extends ValueAsDate > = TValueAsNumber extends true ? number : TValueAsDate extends true ? Date : string; -type InputProps = Omit< - ComponentProps, - keyof RegisterOptions -> & +export type InputProps< + TValueAsNumber extends ValueAsNumber = undefined, + TValueAsDate extends ValueAsDate = undefined +> = Omit, keyof RegisterOptions> & Omit & { name: string; validate?: diff --git a/src/components/HookForm/index.tsx b/src/components/HookForm/index.tsx index 2777192a1..3bbbb5aff 100644 --- a/src/components/HookForm/index.tsx +++ b/src/components/HookForm/index.tsx @@ -1,2 +1,5 @@ -export { default as Form } from './Form'; -export { default as Input } from './Input'; +export { default as experimental_Form } from './Form'; +export { default as experimental_Input } from './Input'; +export { default as experimental_FormFeedback } from './FormFeedback'; +export { default as experimental_FormLabelGroup } from './FormLabelGroup'; +export * from './types'; diff --git a/src/components/HookForm/types.tsx b/src/components/HookForm/types.tsx new file mode 100644 index 000000000..51407e18e --- /dev/null +++ b/src/components/HookForm/types.tsx @@ -0,0 +1,8 @@ +import { BaseSyntheticEvent } from 'react'; +import { UseFormReturn, FieldValues } from 'react-hook-form'; + +export type SubmitHandler = ( + data: TFieldValues, + useFormReturn: UseFormReturn, + event?: BaseSyntheticEvent +) => any | Promise;