-
Notifications
You must be signed in to change notification settings - Fork 994
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(refactor): Split rwjs/forms up into several smaller logical uni…
…ts (#10428)
- Loading branch information
Showing
14 changed files
with
1,028 additions
and
967 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import type { ForwardedRef } from 'react' | ||
import React, { forwardRef } from 'react' | ||
|
||
import type { FieldProps } from './FieldProps' | ||
import { useErrorStyles } from './useErrorStyles' | ||
import { useRegister } from './useRegister' | ||
|
||
export interface CheckboxFieldProps | ||
extends Omit<FieldProps<HTMLInputElement>, 'type' | 'emptyAs'>, | ||
Omit<React.ComponentPropsWithRef<'input'>, 'name' | 'type'> {} | ||
|
||
/** Renders an `<input type="checkbox">` field */ | ||
export const CheckboxField = forwardRef( | ||
( | ||
{ | ||
name, | ||
id, | ||
// for useErrorStyles | ||
errorClassName, | ||
errorStyle, | ||
className, | ||
style, | ||
// for useRegister | ||
validation, | ||
onBlur, | ||
onChange, | ||
...rest | ||
}: CheckboxFieldProps, | ||
ref: ForwardedRef<HTMLInputElement>, | ||
) => { | ||
const styles = useErrorStyles({ | ||
name, | ||
errorClassName, | ||
errorStyle, | ||
className, | ||
style, | ||
}) | ||
|
||
const type = 'checkbox' | ||
|
||
const useRegisterReturn = useRegister( | ||
{ | ||
name, | ||
validation, | ||
onBlur, | ||
onChange, | ||
type, | ||
}, | ||
ref, | ||
) | ||
|
||
return ( | ||
<input | ||
id={id || name} | ||
{...rest} | ||
/** This order ensures type="checkbox" */ | ||
type={type} | ||
{...styles} | ||
{...useRegisterReturn} | ||
/> | ||
) | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import React from 'react' | ||
|
||
import { get, useFormContext } from 'react-hook-form' | ||
|
||
export interface FieldErrorProps | ||
extends React.ComponentPropsWithoutRef<'span'> { | ||
/** | ||
* The name of the field the `<FieldError>`'s associated with. | ||
*/ | ||
name: string | ||
} | ||
|
||
const DEFAULT_MESSAGES = { | ||
required: 'is required', | ||
pattern: 'is not formatted correctly', | ||
minLength: 'is too short', | ||
maxLength: 'is too long', | ||
min: 'is too low', | ||
max: 'is too high', | ||
validate: 'is not valid', | ||
} | ||
|
||
/** | ||
* Renders a `<span>` with an error message if there's a validation error on the corresponding field. | ||
* If no error message is provided, a default one is used based on the type of validation that caused the error. | ||
* | ||
* @example Displaying a validation error message with `<FieldError>` | ||
* | ||
* `<FieldError>` doesn't render (i.e. returns `null`) when there's no error on `<TextField>`. | ||
* | ||
* ```jsx | ||
* <Label name="name" errorClassName="error"> | ||
* Name | ||
* </Label> | ||
* <TextField | ||
* name="name" | ||
* validation={{ required: true }} | ||
* errorClassName="error" | ||
* /> | ||
* <FieldError name="name" className="error" /> | ||
* ``` | ||
* | ||
* @see {@link https://redwoodjs.com/docs/tutorial/chapter3/forms#fielderror} | ||
* | ||
* @privateRemarks | ||
* | ||
* This is basically a helper for a common pattern you see in `react-hook-form`: | ||
* | ||
* ```jsx | ||
* <form onSubmit={handleSubmit(onSubmit)}> | ||
* <input {...register("firstName", { required: true })} /> | ||
* {errors.firstName?.type === 'required' && "First name is required"} | ||
* ``` | ||
* | ||
* @see {@link https://react-hook-form.com/get-started#Handleerrors} | ||
*/ | ||
export const FieldError = ({ name, ...rest }: FieldErrorProps) => { | ||
const { | ||
formState: { errors }, | ||
} = useFormContext() | ||
|
||
const validationError = get(errors, name) | ||
|
||
const errorMessage = | ||
validationError && | ||
(validationError.message || | ||
`${name} ${ | ||
DEFAULT_MESSAGES[validationError.type as keyof typeof DEFAULT_MESSAGES] | ||
}`) | ||
|
||
return validationError ? <span {...rest}>{errorMessage}</span> : null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type { EmptyAsValue, RedwoodRegisterOptions } from './coercion' | ||
|
||
/** | ||
* The main interface, just to have some sort of source of truth. | ||
* | ||
* @typeParam E - The type of element; we're only ever working with a few HTMLElements. | ||
* | ||
* `extends` constrains the generic while `=` provides a default. | ||
* | ||
* @see {@link https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints} | ||
* | ||
* @internal | ||
*/ | ||
export interface FieldProps< | ||
Element extends | ||
| HTMLTextAreaElement | ||
| HTMLSelectElement | ||
| HTMLInputElement = HTMLInputElement, | ||
> { | ||
name: string | ||
id?: string | ||
emptyAs?: EmptyAsValue | ||
errorClassName?: string | ||
errorStyle?: React.CSSProperties | ||
className?: string | ||
style?: React.CSSProperties | ||
validation?: RedwoodRegisterOptions | ||
type?: string | ||
onBlur?: React.FocusEventHandler<Element> | ||
onChange?: React.ChangeEventHandler<Element> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import React, { forwardRef } from 'react' | ||
import type { ForwardedRef } from 'react' | ||
|
||
import { useForm, FormProvider } from 'react-hook-form' | ||
import type { FieldValues, UseFormReturn, UseFormProps } from 'react-hook-form' | ||
|
||
import { ServerErrorsContext } from './ServerErrorsContext' | ||
|
||
export interface FormProps<TFieldValues extends FieldValues> | ||
extends Omit<React.ComponentPropsWithRef<'form'>, 'onSubmit'> { | ||
error?: any | ||
/** | ||
* The methods returned by `useForm`. | ||
* This prop is only necessary if you've called `useForm` yourself to get | ||
* access to one of its functions, like `reset`. | ||
* | ||
* @example | ||
* | ||
* ```typescript | ||
* const formMethods = useForm<FormData>() | ||
* | ||
* const onSubmit = (data: FormData) => { | ||
* sendDataToServer(data) | ||
* formMethods.reset() | ||
* } | ||
* | ||
* return ( | ||
* <Form formMethods={formMethods} onSubmit={onSubmit}> | ||
* ) | ||
* ``` | ||
*/ | ||
formMethods?: UseFormReturn<TFieldValues> | ||
/** | ||
* Configures how React Hook Form performs validation, among other things. | ||
* | ||
* @example | ||
* | ||
* ```jsx | ||
* <Form config={{ mode: 'onBlur' }}> | ||
* ``` | ||
* | ||
* @see {@link https://react-hook-form.com/api/useform} | ||
*/ | ||
config?: UseFormProps<TFieldValues> | ||
onSubmit?: (value: TFieldValues, event?: React.BaseSyntheticEvent) => void | ||
} | ||
|
||
/** | ||
* Renders a `<form>` with the required context. | ||
*/ | ||
function FormInner<TFieldValues extends FieldValues>( | ||
{ | ||
config, | ||
error: errorProps, | ||
formMethods: propFormMethods, | ||
onSubmit, | ||
children, | ||
...rest | ||
}: FormProps<TFieldValues>, | ||
ref: ForwardedRef<HTMLFormElement>, | ||
) { | ||
const hookFormMethods = useForm<TFieldValues>(config) | ||
const formMethods = propFormMethods || hookFormMethods | ||
|
||
return ( | ||
<form | ||
ref={ref} | ||
{...rest} | ||
onSubmit={formMethods.handleSubmit((data, event) => | ||
onSubmit?.(data, event), | ||
)} | ||
> | ||
<ServerErrorsContext.Provider | ||
value={ | ||
errorProps?.graphQLErrors?.[0]?.extensions?.properties?.messages || {} | ||
} | ||
> | ||
<FormProvider {...formMethods}>{children}</FormProvider> | ||
</ServerErrorsContext.Provider> | ||
</form> | ||
) | ||
} | ||
|
||
// Sorry about the `as` type assertion (type cast) here. Normally I'd redeclare | ||
// forwardRef to only return a plain function, allowing us to use TypeScript's | ||
// Higher-order Function Type Inference. But that gives us problems with the | ||
// ForwardRefExoticComponent type we use for our InputComponents. So instead | ||
// of changing that type (because it's correct) I use a type assertion here. | ||
// forwardRef is notoriously difficult to use with UI component libs. | ||
// Chakra-UI also says: | ||
// > To be honest, the forwardRef type is quite complex [...] I'd recommend | ||
// > that you cast the type | ||
// https://github.com/chakra-ui/chakra-ui/issues/4528#issuecomment-902566185 | ||
export const Form = forwardRef(FormInner) as <TFieldValues extends FieldValues>( | ||
props: FormProps<TFieldValues> & React.RefAttributes<HTMLFormElement>, | ||
) => React.ReactElement | null |
Oops, something went wrong.