Form
The field primitives (Field, Label,
Input, FieldError) do not care where a
value comes from. Form and FormField connect them to
react-hook-form so a validated field is a
few lines and the error wiring is automatic.
import Form from '@zuilib/primitives/form'
import FormField from '@zuilib/primitives/form-field'
import { useAutosave } from '@zuilib/primitives/lib/use-autosave'
react-hook-form is an optional peer of @zuilib/primitives. Install it
when you import these subpaths; nothing else in the package needs it.
pnpm add react-hook-form
A validated field
Form wraps FormProvider and a <form> whose submit runs
form.handleSubmit(onSubmit). FormField is a Controller that renders
its function child inside a Field: the label, the control
and the error are associated automatically, and the field turns invalid
whenever it has an error.
Submit the empty form to see the error appear and the input turn invalid.
Controlled components
Checkbox, Switch, Select, Combobox and RadioGroup are not native
inputs, so spread nothing. Wire checked or value and the change handler
from field by hand:
<FormField control={form.control} name="terms">
{({ field }) => (
<Checkbox checked={field.value} onCheckedChange={field.onChange} label="I agree" />
)}
</FormField>
Autosave
onAutosave on a FormField debounces a save of that field's value and
exposes the state as the autosave render prop. The same behaviour is
available standalone as useAutosave.
<FormField control={form.control} name="bio" onAutosave={(value) => api.patchBio(value)} autosaveDebounceMs={800}>
{({ field, autosave, error }) => (
<>
<Label unsaved={autosave?.status === 'unsaved'}>Bio</Label>
<Textarea {...field} />
<FieldError error={error} />
</>
)}
</FormField>
const { status, lastSaved, error, retry } = useAutosave({
value: watchedValue,
onAutosave: async (value) => api.save(value),
debounceMs: 1000,
})
// status: 'idle' | 'unsaved' | 'saving' | 'saved' | 'error'
Props
FormProps extends form props (minus onSubmit).
| Prop | Type | Description |
|---|---|---|
form | UseFormReturn<TFieldValues> | The useForm() result; rendered as a FormProvider |
onSubmit | (data: TFieldValues) => void | Promise<void> | Wired through form.handleSubmit |
children | ReactNode |
FormFieldProps extends react-hook-form's ControllerProps (minus render).
| Prop | Type | Default | Description |
|---|---|---|---|
control | Control<TFieldValues> | — | From useForm(); falls back to the enclosing Form |
name | FieldPath<TFieldValues> | — | |
children | (props: FormFieldRenderProps) => ReactNode | — | A function child, not render |
onAutosave | (value) => Promise<void> | — | Enables a debounced save of the field value |
autosaveDebounceMs | number | 1000 |
FormFieldRenderProps:
| Property | Type | Use |
|---|---|---|
field | ControllerRenderProps | Spread on a native control, or read value / onChange for a controlled one |
error | FieldError | undefined | Pass to FieldError |
ids | { control, description, message } | Optional: wire htmlFor / id by hand. An explicit id wins over the generated one |
autosave | AutosaveState | undefined | Present when onAutosave is set |
AutosaveState is { status, lastSaved, error, retry }.
Accessibility
FormFieldrenders a Field, sofor,aria-labelledbyandaria-describedbyare generated; no ids needed.- The field is
invalidwhile it has an error: every control inside getsaria-invalidand the error styling. FieldErroris a live region, so a validation message is announced when it appears.
Related
- Field: what
FormFieldrenders around your control. - FieldError: takes the
errorrender prop directly. - Toast:
toast.erroron a failed submit.