react-data-form
Anatomy of a form
useForm, FormElement, and the round trip a value makes between them.
Two halves#
A form is always the same two calls. useForm takes the description and gives back a form context — the built form, its current data, and the handlers. FormElement takes that context and renders it.
const formContext = useForm({
form, // the description
data, // initial values, as your API hands them over
onChange, // fires on every keystroke
onSubmit, // fires once the validators pass
asyncData, // a promise resolving to the initial values
})Keeping them apart is what lets a form be rendered somewhere other than where it is built: a filter bar in a toolbar, a sub-form inside a dialog, a wizard whose navigation lives in a footer.
What useForm returns#
| Name | Type | Default | Description |
|---|---|---|---|
form | FormBuiltInterface | — | The built form: every field normalised, given a name, and carrying its current value and violations. |
onChange | (input: FormInputInterface) => void | — | What a controller calls. It updates one field and rebuilds the form around it. |
onSubmit | () => Promise<Data | undefined> | — | Validates, and resolves with the payload — or with undefined when a validator threw. |
updateData | (data: Data, partial?: boolean) => void | — | Writes values in from the outside. Partial by default, so a subset merges. |
updateForm | (form: FormBuiltInterface) => FormBuiltInterface | — | Replaces the description itself — how steps move, and how a field can add another. |
ready | boolean | — | False until asyncData (or form.getData) has resolved. True immediately when there is neither. |
isLoading | BooleanStateInterface | — | True while a submit is in flight; the submit button reads it. |
onSuccess | PubSub<Data> | — | Publishes after every successful submit — how a dialog knows to close. |
The round trip of a value#
Nothing in the library reads a DOM event. A controller is handed the field and a callback, and the loop is closed by the context:
FormInputswalksform.inputsand renders each field through its controller.- The controller calls
onChange({ ...formInput, value })— the whole field, not just the value. useFormmerges it back into the form and re-renders. A field that isreadonlyis ignored here, which is where read-only is actually enforced.- On submit, every
validatorruns, violations are attached to their fields, andonSubmitonly fires if none was raised.
Why the whole field, not just the value
FormElement is four components#
FormElement is a convenience, and a very thin one. It is exactly this:
import {
FormProvider,
FormDecorator,
FormHeader,
FormInputs,
FormErrors,
FormSubmitAction,
} from "react-data-form"
// This *is* FormElement — nothing more.
<FormProvider formContext={formContext}>
<FormDecorator>
<FormHeader />
<FormInputs />
<FormErrors />
<FormSubmitAction />
</FormDecorator>
</FormProvider>Which means you can assemble those pieces yourself when the layout calls for it — or swap a single one through the form's components key and keep the rest:
useForm({
form: {
inputs: { email: { label: "Email", type: "email" } },
components: {
// Replace one piece, keep the rest.
formSubmitAction: MyOwnSubmitBar,
},
},
})| Name | Type | Default | Description |
|---|---|---|---|
components.formSubmitAction | FC | — | The submit bar. Replaced by the step navigation in a wizard. |
components.formInputs | FC | — | How fields are laid out. createGroupForm swaps this one to get sections. |
components.formDecorator | FC<FormDecoratorPropsInterface> | — | The wrapping element — the <form> tag and its layout. |
components.formErrors | FC | — | Form-level errors, as opposed to per-field violations. |
components.formGroupProvider | FC<FormGroupProviderPropsInterface> | — | The wrapper around each field — its label, description and violation. |
Loading initial values#
A create form starts empty; an edit form starts with what the API has. Pass data when you already hold it, and asyncData when you do not:
const formContext = useForm({
form: { inputs: { name: { label: "Name" } } },
// The form renders straight away; `ready` flips once the promise settles.
asyncData: () => api.get("/me").then((response) => response.data),
})
if (!formContext.ready) return <Loader />ready is not isLoading
ready is about the initial values and flips once. isLoading is about a submit in flight and flips on every submission. Rendering a spinner on the wrong one gives a form that disappears every time it is saved.
Saving on change#
Some forms have no submit button — a settings panel, a filter bar. Set saveOnChange and the form submits itself, debounced, 1.5 seconds after the last edit.
{
saveOnChange: true, // submits 1.5s after the last keystroke
onSubmit: (data) => api.patch("/settings", data),
}saveOnChange — edit a field and stop typing
This is exactly how the filter bar of react-resource-view works: it is an ordinary form with saveOnChange, whose onChange rewrites the query string.
Next
The next page goes through every key a field description accepts, and what each one changes on screen.