Skip to main content

form — inputs, actions and one submitted object

UiForm is the editable counterpart to detail: a <form> element holding field nodes, a footer of actions and optional links. It is the node a trigger's payload points at — name a form id there and the client collects every named control inside it and sends them as one JSON object.

A form has two bodies. fields is the plain vertical list, which covers most screens. content takes arbitrary nodes — a stack for columns, a section for tabs, fieldgroups — for when the layout matters. Both submit together, because the payload is collected from the DOM, not from the fields list.

Live — press "Save" to see the form-level banner plus a per-field error on every input; "Cancel" clears them again. Neither button calls a server: both run a client handler that returns a patch.

Fields

FieldTypeMeaning
idStringNode id, the DOM id of the <form> — and the value you put in a trigger's payload.
titleStringRendered as an <h2> above the fields. Omit for an untitled form.
fieldsList<UiField>The flat, vertical list of inputs. Empty by default.
contentList<UiNode>Rich body rendered after fields — any node tree, for columns, tabs or groups.
actionsList<UiAction>Footer buttons. The first PRIMARY one (else the first) supplies the native method/action fallback.
linksList<UiLink>Footer links, rendered next to the actions.
formErrorStringForm-level error banner above the fields, role="alert". For cross-field or save failures.
reloadOnSubmitbooleantrue makes submit a native full-page navigation instead of an event-bus fetch.
cssClassStringExtra CSS class on the <form>.

Building one

UiForm.of("product-form", "New product")
.field(UiField.text("name", "Name", null).asRequired().asEditable()
.placeholder("e.g. Widget"))
.field(UiField.number("price", "Price", 19.0).asEditable().step("0.01"))
.field(UiField.select("category", "Category", "tools", List.of(
UiField.Option.of("tools", "Tools"),
UiField.Option.of("toys", "Toys"))).asEditable())
.field(UiField.bool("active", "Active", true).asEditable())
// The payload id is this form's id — the client collects it on submit.
.action(UiAction.primary("save", "Save")
.onClick(UiTrigger.api("POST", "/products", "product-form")))
.action(UiAction.secondary("cancel", "Cancel").onClick(UiTrigger.go("/products")))
.link(UiLink.of("ref", "/help/products", "Need help?"));

// Redisplaying a rejected submit:
form.error("Please fix the errors below.");

// A two-column body instead of the flat list:
UiForm.of("customer-form", "Customer")
.content(UiFieldGroup.of("contact", "Contact")
.field(UiField.text("email", "E-mail", null).asEditable()))
.content(UiStack.of(left, right).direction(UiStack.Direction.HORIZONTAL).gap(16));

// Theme switch and other changes that live outside #sui-root:
UiForm.of("theme-form", null).reloadOnSubmit();

Notes

The payload is the form id, not the field list. A trigger with payload: "product-form" makes the client walk every named control inside that <form> element. Fields nested in content, inside a fieldgroup, or on a hidden tab all ride along — layout never changes the submitted shape.

Two levels of error. formError is the banner for cross-field and save-level problems; per-input messages belong on UiField.validationError. The usual server flow is: reject the submit, set both, return the same form as the response — the renderer swaps it in place.

Only editable fields are inputs. UiField.editable defaults to false, which renders a plain value span with no name — so it is not submitted. A form of non-editable fields is a read-only card; use detail when that is the intent.

reloadOnSubmit is an escape hatch. Normally the event bus intercepts the submit, fetches JSON and patches the page. Set this flag only when the effect lives outside the mounted #sui-root subtree — swapping the stylesheet in <head>, or replacing the SPA bootstrap with an SSR response.

Actions double as the no-JS fallback. The renderer copies method and action onto the <form> from the primary action's trigger, tunnelling PUT/DELETE through a hidden _method input. A form without actions has no native submit target.

See also

  • Forms — the conceptual guide: layout, validation round-trips, patterns.
  • field — the inputs themselves and every FieldType.
  • fieldgroup — titled <fieldset> grouping inside content.
  • upload — drag-and-drop file intake.
  • action — the footer buttons.
  • Triggers & actions — how payload and the behaviours work.