react-data-form

Fields

Every key of a field description, and what each one changes on screen.

The key is the name#

inputs is a record. Its keys are the field names, and they are the keys your onSubmit handler receives — there is no mapping step in between.

inputs: {
  // The key is the field's name, and the key of the submitted payload.
  emailAddress: { label: "Email", type: "email" },
}
// → onSubmit receives { emailAddress: "…" }

Every key of a field#

NameTypeDefaultDescription
labelstring | ReactNode | nullShown above the control. Passed through the translation dictionary. null renders no label at all.
descriptionstring | ReactNodeHelp text under the control.
placeholderstringPlaceholder of the underlying input.
type"text" | "email" | "number" | "password" | "checkbox" | "hidden" | …"text"Only read by DefaultInputController, which is what a field falls back to. hidden keeps the value out of the page.
controllerFC<InputControllerInterface>DefaultInputControllerThe component that renders the field.
requiredbooleanMarks the field as required and passes the attribute to the control.
readonlybooleanChanges are dropped by useForm, so the value cannot move even if a controller tries.
defaultValueValue | ((current) => Value)Applied when the form is built and no data carries the field.
valueValueThe current value. Normally the library's to set, not yours.
valueOptionsValueOptionInterface[]Static choices, for any of the select-like controllers.
getValueOptions(input?) => Promise<ValueOptionInterface[]>Choices fetched once, when the field first renders.
onSearch(query, formContext?) => Promise<ValueOptionInterface[]>Choices fetched per keystroke — the search and autocomplete controllers.
validator(value: Value) => Value | neverThrows to reject. Zod errors are unpacked into one violation per issue. See Validation.
violationsViolationInterface[]Errors currently attached to the field, from a validator or from your API.
hidden() => booleanRe-evaluated on render, so a field can appear as another one changes.
ordernumberAscending. Fields without one keep their declaration order.
groupsstring[]Which sections the field belongs to. See Groups and Steps.
formFormInterfaceRenders the field as a nested sub-form. See Nested forms.
getForm() => FormInterfaceThe same, resolved lazily — for a recursive or self-referencing shape.
generatedValuebooleanExcluded from validation and from the table's columns. For values injected by context.
min / maxnumberBounds, read by the number, slider and date controllers.
components.decoratorFC<BaseDecoratorFormInputInterface>Replaces the wrapper around this one field.

type, without a controller#

A field with no controller falls back to DefaultInputController, an HTML <input> driven by its type. That covers the ordinary cases with no imports at all.

inputs: {
  name:     { label: "Name" },                     // text
  age:      { label: "Age", type: "number" },
  email:    { label: "Email", type: "email" },
  secret:   { label: "Password", type: "password" },
  agreed:   { label: "I agree", type: "checkbox" },
  internal: { type: "hidden" },                    // never rendered
}

Plain types, no controllers

Choices#

Every select-like controller reads the same shape — a list of { label, value } — from one of three keys, depending on when the choices are known.

import { valueOptionMapper, valueOptionFromArray } from "react-data-form"

// Written by hand
valueOptions: [
  { label: "Draft", value: "draft" },
  { label: "Published", value: "published" },
]

// From a plain array
valueOptions: valueOptionFromArray(["S", "M", "L"])

// From records, naming the label and value keys
valueOptions: valueOptionMapper(authors, "name", "@id")

// Fetched, once, when the field first renders
getValueOptions: async () => valueOptionMapper(await api.authors(), "name", "@id")

// Fetched on every keystroke — for a search field
onSearch: async (query) => valueOptionMapper(await api.authors({ query }), "name", "@id")
NameTypeDefaultDescription
labelrequiredstring | ReactNodeWhat the reader sees.
valuerequiredstring | number | booleanWhat ends up in the payload.
descriptionstringA second line, rendered by the card and radio controllers.
groupstringGroups options under a heading in the dropdown.
aliasesstring[]Extra terms the option matches when searching.
originalTThe record the option was built from — valueOptionMapper keeps it, so a custom item component can read the rest of it.

Static options

Showing a field conditionally#

hidden is a function, not a boolean, and it is called on every render. That is what lets one field depend on another without any subscription mechanism.

inputs: {
  shipping: {
    label: "Shipping method",
    controller: SelectInputController,
    valueOptions: [
      { label: "Pickup", value: "pickup" },
      { label: "Delivery", value: "delivery" },
    ],
  },
  address: {
    label: "Delivery address",
    // Re-evaluated on every render, so it follows the field above.
    hidden: () => getFormInputInForm(form, "shipping")?.value !== "delivery",
  },
}

Hidden is not absent

A hidden field keeps its value and still ships in the payload. To drop it entirely, leave it out of inputs.

Ordering#

Fields render in declaration order until one of them carries an order. Useful when a form is assembled from several sources — a base description plus additions from a plugin.

inputs: {
  reference: { label: "Reference", order: 30 },
  title:     { label: "Title", order: 10 },
  notes:     { label: "Notes", order: 20 },
}
// Rendered: Title, Notes, Reference.

Generated values#

generatedValue: true marks a field whose value comes from the surrounding context rather than the reader. Two things follow:

  • validation skips it — nobody typed it, so nobody can fix it;
  • the table layout leaves it out of the columns, and the filter bar leaves it out of “clear search”.

Next

The catalogue of controllers is next — the part of the library you will come back to most.