Forms
Redwood provides several helpers to make building forms easier. All of Redwood's helpers are simple wrappers around React Hook Form (RHF) that make it even easier to use in most cases.
If Redwood's helpers aren't flexible enough for you, you can use React Hook Form directly. @redwoodjs/forms
exports everything it does:
import {
useForm,
useFormContext,
/**
* Or anything else React Hook Form exports!
*
* @see {@link https://react-hook-form.com/api}
*/
} from '@redwoodjs/forms'
Overview
@redwoodjs/forms
exports the following components:
Component | Description |
---|---|
<Form> | Surrounds all components, providing form and error contexts |
<FormError> | Displays error messages from the server. Typically placed at the top of your form |
<Label> | Used in place of the HTML <label> tag. Accepts error-styling props |
<InputField> | Used in place of the HTML <input> tag. Accepts validation and error-styling props (also see the list of input field components enumerated below) |
<SelectField> | Used in place of the HTML <select> tag. Accepts validation and error-styling props |
<TextAreaField> | Used in place of the HTML <textarea> tag. Accepts validation and error-styling props |
<FieldError> | Displays error messages if the field with the same name prop has validation errors. Only renders if there's an error on the associated field |
<Submit> | Used in place of <button type="submit"> . Triggers validation and "submission" (executes the function passed to <Form> 's onSubmit prop) |
All HTML <input>
types are also available as components. They follow the naming convention <TypeField>
where Type
is one of the HTML input types.
We'll refer to them collectively as "input fields".
The full list is:
<ButtonField>
<CheckboxField>
<ColorField>
<DateField>
<DatetimeLocalField>
<EmailField>
<FileField>
<HiddenField>
<ImageField>
<MonthField>
<NumberField>
<PasswordField>
<RadioField>
<RangeField>
<ResetField>
<SearchField>
<SubmitField>
<TelField>
<TextField>
<TimeField>
<UrlField>
<WeekField>
Validation and Error-styling Props
All components ending in Field
(i.e. all input fields, along with <SelectField>
and <TextAreaField>
) accept validation and error-styling props.
By validation and error-styling props, we mean three props specifically:
validation
, which accepts all of React Hook Form'sregister
options, plus the Redwood-exclusive coercion helpersvalueAsBoolean
,valueAsJSON
errorClassName
anderrorStyle
, which are the classes and styles to apply if there's an error
Besides name
, all other props passed to these components are forwarded to the tag they render.
Here's a table for reference:
Prop | Description |
---|---|
name | The name of the field. React Hook Form uses it a key to hook it up with everything else |
validation | All your validation logic. Accepts all of React Hook Form's register options, plus the Redwood-exclusive coercion helpers valueAsBoolean , valueAsJSON |
errorClassName | The class name to apply if there's an error |
errorStyle | The style to apply if there's an error |
Example
A typical React component using these helpers would look something like this:
import {
Form,
Label,
TextField,
TextAreaField,
FieldError,
Submit,
} from '@redwoodjs/forms'
const ContactPage = () => {
const onSubmit = (data) => {
console.log(data)
}
return (
<Form onSubmit={onSubmit}>
<Label name="name" className="label" errorClassName="label error" />
<TextField
name="name"
className="input"
errorClassName="input error"
validation={{ required: true }}
/>
<FieldError name="name" className="error-message" />
<Label name="email" className="label" errorClassName="label error" />
<TextField
name="email"
className="input"
errorClassName="input error"
validation={{
required: true,
pattern: {
value: /[^@]+@[^\.]+\..+/,
},
}}
/>
<FieldError name="email" className="error-message" />
<Label name="message" className="label" errorClassName="label error" />
<TextAreaField
name="message"
className="input"
errorClassName="input error"
validation={{ required: true }}
/>
<FieldError name="message" className="error-message" />
<Submit className="button">Save</Submit>
</Form>
)
}
<Form>
Any form you want Redwood to validate and style in the presence errors should be surrounded by this tag.
Prop | Description |
---|---|
config | Accepts an object containing options for React Hook Form's useForm hook |
formMethods | The functions returned from useForm . You only need to use this prop if you need to access to one of the functions that useForm returns (see example below) |
onSubmit | Accepts a function to be called if validation succeeds. Called with an object containing name-value pairs of all the fields in your form |
All other props are forwarded to the <form>
tag that it renders.
<Form>
Explained
<Form>
encapsulates React Hook Form's useForm
hook and <FormProvider>
context, along with Redwood's ServerError
context.
It's hard to talk about this component without getting into the nitty-gritty of React Hook Forms.
useForm
is React Hook Form's major hook.
It returns a bunch of functions, one of which is register
, which you use to quite literally "register" fields into React Hook Form so it can validate them.
(This has to do with controlled vs. uncontrolled components. React Hook Form takes the latter approach.)
All of Redwood's form helpers need the register
function to do what they do. But they don't get it straight from <Form>
because they could be nested arbitrarily deep. That's where <FormProvider>
comes in: by passing the functions returned from useForm
to <FormProvider>
, Redwood's helpers can just use useFormContext
to get what they need.
Using formMethods
There's some functions that useForm
returns that it'd be nice to have access to.
For example, useForm
returns a function reset
, which resets the form's fields.
To access it, you have to call useForm
yourself.
But you still need to pass useForm
's return to the <FormProvider>
so that Redwood's helpers can register themselves:
import { useForm } from 'react-hook-form'
const ContactPage = () => {
const formMethods = useForm()
const onSubmit = (data) => {
console.log(data)
formMethods.reset()
}
return (
<Form formMethods={formMethods} onSubmit={onSubmit}>
// Still works!
<TextField name="name" validation={{ required: true }}>
</Form>
)
}
<FormError>
This helper renders a <div>
containing a "title" message and a <ul>
enumerating any errors reported by the server when trying to save your form. You can see it in a scaffold if you submit a form that somehow gets passed client-side validation:
For example, let's say you have a form with a <TextField>
for a user's email address, but you didn't specify any validation on it:
import { useMutation } from '@redwoodjs/web'
const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: ContactInput!) {
createContact(input: $input) {
id
}
}
`
const ContactPage = () => {
const [create, { loading, error }] = useMutation(CREATE_CONTACT)
const onSubmit = (data) => {
create({ variables: { input: data }})
}
return (
<Form onSubmit={onSubmit}>
<FormError error={error}>
// No validation—any email goes!
<TextField name="email" />
</Form>
)
}
Since there's no validation, anything goes!
On the client at least.
GraphQL is built on types, so it's not going to let just anything through.
Instead it'll throw an error and bubble it back up to the top (via the error
object returned by the useMutation
hook) where <FormError>
can render something like:
<div>
<p>
Can't create new contact:
</p>
<ul>
<li>
email is not formatted like an email address
</li>
</ul>
</div>
<Label>
Renders an HTML <label>
tag with different className
and style
props depending on whether the field it's associated with has a validation error.
This tag can be self-closing, in which case the name
becomes the text of the label:
<Label name="name" className="input" errorClassName="input error" />
<!-- Renders: <label for="name" class="input">name</label> -->
It can also have standard separate open/close tags and take text inside, in which case that text is the text of the rendered <label>
:
<Label name="name" className="input" errorClassName="input error">Your Name</Label>
<!-- Renders: <label for="name" class="input">Your Name</label> -->
All props are passed to the underlying <label>
tag besides the ones listed below:
Prop | Description |
---|---|
name | The name of the field that this label is associated with. This should be the same as the name prop on the input field this label is for |
errorClassName | The className that's used if the field with the same name has a validation error |
errorStyle | The style that's used if the field with the same name has a validation error |
Input Fields
Inputs are the backbone of most forms.
While you can use <InputField>
and it's type
prop to make all the different kinds of input fields you'd use in a form, it's often easier to reach for the named input fields (listed above) which have defaults for things like coercion configured where appropriate.
Default coercion
Certain input fields handle coercion automatically, but you can always override the coercion or, if it's not built-in, set it manually via the validation
prop's setValueAs property.
The input fields that coerce automatically are:
Field | Default coercion |
---|---|
<CheckboxField> | valueAsBoolean |
<NumberField> | valueAsNumber |
<DateField> | valueAsDate |
<DatetimeLocalField> | valueAsDate |
valueAsDate
and valueAsNumber
are built into React Hook Form and are based on the HTML standard.
But because Redwood uses GraphQL on the backend, it's important that the types submitted by the form be what the GraphQL server expects.
Instead of forcing users to make heavy-use of setValueAs
for custom coercion, Redwood extends react hook form's valueAs
properties with two more for convenience:
valueAsBoolean
valueAsJSON
Another nice thing Redwood does for you is automatically treat empty strings (''
) as undefined
for text input fields whose name ends in Id
to make it easier to work with fields for optional database relations. You can disable this by marking the field required, by supplying your own setValueAs
function, or by simply renaming the field.
<SelectField>
Renders an HTML <select>
tag.
It's possible to select multiple values using the multiple
prop.
When multiple
is true
, this field returns an array of values in the same order as the list of options, not in the order they were selected.
<SelectField name="toppings" multiple={true}>
<option>'lettuce'</option>
<option>'tomato'</option>
<option>'pickle'</option>
<option>'cheese'</option>
</SelectField>
// If the user chooses lettuce, tomato, and cheese,
// the onSubmit handler receives:
//
// { toppings: ["lettuce", "tomato", "cheese"] }
//
Validation
In these two examples, one with multiple-field selection, validation requires that a field be selected and that the user doesn't select the first value in the dropdown menu:
<SelectField
name="selectSingle"
validation={{
required: true,
validate: {
matchesInitialValue: (value) => {
return (
value !== 'Please select an option' ||
'Select an Option'
)
},
},
}}
>
<option>Please select an option</option>
<option>Option 1</option>
<option>Option 2</option>
</SelectField>
<FieldError name="selectSingle" style={{ color: 'red' }} />
<SelectField
multiple={true}
name="selectMultiple"
validation={{
required: true,
validate: {
matchesInitialValue: (value) => {
let returnValue = [true]
returnValue = value.map((element) => {
if (element === 'Please select an option')
return 'Select an Option'
})
return returnValue[0]
},
},
}}
>
<option>Please select an option</option>
<option>Option 1</option>
<option>Option 2</option>
</SelectField>
<FieldError name="selectMultiple" style={{ color: 'red' }} />
Coercion
Typically, a <SelectField>
returns a string, but you can use one of the valueAs
properties to return another type.
An example use-case is when <SelectField>
is being used to select a numeric identifier.
Without the valueAsNumber
property, <SelectField>
returns a string.
But, as per the example below, the valueAsNumber
can be used to return an Int
:
<SelectField name="select" validation={{ valueAsNumber: true }}>
<option value={1}>Option 1</option>
<option value={2}>Option 2</option>
<option value={3}>Option 3</option>
</SelectField>
If Option 3
is selected, the <Form>
's onSubmit
function is passed data as follows:
{
select: 3,
}
As with input fields, Redwood will automatically treat empty strings (''
) as undefined
for select fields whose name ends in Id
. See the previous section about this for more info.
<FieldError>
Renders a <span>
containing a validation error message if the field with the same name
attribute has a validation error. Otherwise renders nothing.
<FieldError name="name" className="error-message">
<!-- Renders: <span class="error-message">name is required</span> -->