Files
dify/packages/dify-ui/docs/forms.md

103 lines
5.1 KiB
Markdown

# Forms
Dify UI form primitives compose Base UI's native form semantics, field accessibility, and Dify
styling. They are not a form state-management or schema framework. See the [Base UI forms
handbook] for the upstream model.
## Submit boundary
Every group of controls that saves or submits together needs a real `<form>` boundary. Do not wire
an `Input` and a click-only `Button` together as an informal form.
Use `Form` when the Dify UI boundary should own Base UI's structured `onFormSubmit` values,
consolidated errors, `actionsRef`, or `validationMode`. It renders a native `<form>`. A native form
remains correct when another form library owns submission and validation; do not nest form owners.
Set [`Button`] submit buttons to `type="submit"` explicitly. Keep every other button inside a form
at `type="button"`.
Keep submit available while the form is incomplete; submitting is what runs validation and marks
the fields. Once a submission starts, the submit button shows `loading` until it settles.
A rejected submission reports at the field it concerns: map a documented error code to that
field's message through `Form` `errors`, rather than showing the server message or a toast.
Rejections that belong to no field stay inside the form as an alert.
## Value and state ownership
`Form` owns the submission and validation boundary described above, not each field's draft.
Choose controlledness from source-of-truth needs, independently from where a draft is stored.
Prefer `defaultValue` when application React code does not need to own the current value. Use
`value` and change handlers when application rendering or coordination must own it while editing.
Listening to change events, tracking dirty state, and native or primitive validation do not by
themselves require controlled state.
Application code owns the draft in the narrowest component whose lifetime matches it. A value can
be controlled locally without being lifted. An uncontrolled field can participate in a persisted
workflow when that workflow captures its value at an explicit persistence boundary. The
surrounding surface defines its mount lifecycle; owner placement determines whether draft state
lives inside or outside that lifecycle.
## Fields and labels
Use `Field` when a control needs a shared name, label, validation, description, or error state. A
standalone `Input` may use a native `<label htmlFor>` relationship, but normal form rows should
prefer a visible label. `FieldDescription` and `FieldError` provide the corresponding accessible
message relationships.
`FieldError` follows native constraints, `validate`, and `Form` `errors`. When application code
owns the validity instead, set `invalid` on `Field` and `match` on `FieldError`. A `Field` describes
one control; a composite that renders its own controls, such as a picker with controls in its popup,
does not go inside one.
Choose the label primitive by the control:
- Text-like inputs, `Textarea`, input-based `Combobox` and `Autocomplete`, a single `Checkbox`,
each `Radio` option, `Switch`, and `NumberField` use `FieldLabel`.
- Trigger-based `Select` fields use `SelectLabel`.
- Date/time pickers use their own `DatePickerLabel`, `TimePickerLabel` or `DateTimePickerLabel`
to focus the trigger without opening it. They participate in Base UI Field/Form and native forms; see the [picker field contract].
- `Slider` fields follow the [Base UI Slider anatomy] and use `SliderLabel`; only multi-thumb
sliders add per-thumb `aria-label` to distinguish the thumbs.
- `SelectGroupLabel` and `AutocompleteGroupLabel` label option groups inside popup content. They
are not field labels.
Use [`InputGroup`] when a prefix, suffix, or action shares the input's visual surface.
## Grouped controls
Use `Fieldset` and `FieldsetLegend` when one field contains related controls, such as checkbox or
radio groups, multi-thumb sliders, or a section of related inputs. Wrap each checkbox or radio
option with `FieldItem` and give it its own label:
```tsx
<Field name="allowedNetworkProtocols">
<Fieldset render={<CheckboxGroup />}>
<FieldsetLegend>Allowed network protocols</FieldsetLegend>
<FieldItem>
<FieldLabel className="flex items-center gap-2">
<Checkbox value="https" />
HTTPS
</FieldLabel>
</FieldItem>
</Fieldset>
</Field>
```
`Fieldset` owns group semantics and the legend relationship, not interactive state. Pass
`disabled`, `value`, `defaultValue`, and change handlers to the group primitive.
Every radio belongs to a `RadioGroup`. Use `FieldsetLegend` to name the group and `FieldLabel` to
name each option; do not render a standalone `Radio`.
Keep form state, schemas, server validation, and reset behavior outside the primitive internals,
in the nearest application owner with the required lifetime. Pass observable state through the
public field and control props instead of replacing the semantic structure.
[Base UI Slider anatomy]: https://base-ui.com/react/components/slider#anatomy
[Base UI forms handbook]: https://base-ui.com/react/handbook/forms
[`Button`]: ../src/button/README.md
[`InputGroup`]: ../src/input-group/README.md
[picker field contract]: ../src/date-time/README.md#anatomy-and-labels