Skip to main content

field — one labelled input

UiField is a single labelled value: a label, an optional hint, an optional error, and one control whose kind is chosen by fieldType. It is the only node that produces a submittable input, and it does so only when editable is true — otherwise it renders the value as plain text.

Fields normally sit in a form's fields list or a detail, but they are ordinary nodes: drop them anywhere in a layout tree and, as long as they end up inside a <form> element, they are collected with the rest of the payload.

Live — six field types plus a read-only one ("SKU", no editable) and one carrying a validationError. Toggling "Active" fires its onChange trigger, which reports the new value without submitting anything.

Fields

FieldTypeMeaning
idStringNode id. Becomes the wrapper's DOM id and the control's name — so it is the key in the submitted payload. The control itself gets <id>__input.
labelStringThe <label> text. Required in practice.
fieldTypeFieldTypeWhich control to render. See the table below.
valueObjectCurrent value. Rendered as text when not editable.
editablebooleanDefaults to false (read-only span). true renders a real input.
requiredbooleanAppends a * marker to the label.
placeholderStringPlaceholder text. Emitted for TEXT, REFERENCE and any unrecognised type.
hintStringSmall helper text below the control.
iconStringLeading in-field icon token. Decorative, and only rendered when editable. See icons.
trailingUiActionAction rendered on the control's row, right of the input — a Browse… next to a path field, an Encrypt next to a key field. Editable fields only.
validationErrorStringPer-field error message; also puts the wrapper into its error style.
optionsList<Option>Choices for SELECT / MULTISELECT. Each Option has value and label.
minStringLower bound, verbatim min attribute. Numeric and date types.
maxStringUpper bound, verbatim max attribute.
stepStringStep granularity, verbatim step attribute (e.g. "0.01").
submitOnEnterbooleanTEXTAREA only: Enter submits the surrounding form, Shift+Enter inserts a newline.
submitOnChangebooleanAny change to the value submits the surrounding form.
onChangeUiTriggerTrigger fired on change, carrying the surrounding form's values as payload. Preferred over submitOnChange when only part of the UI should react.
acceptStringFILE only: the HTML accept filter ("image/*", ".pdf,.docx").
multiplebooleanFILE only: allow selecting more than one file.
cssClassStringExtra CSS class on the field wrapper.

Field types

fieldTypeRenders asExtra fields that apply
TEXT<input type="text">placeholder, icon
TEXTAREA<textarea rows="4">submitOnEnter
PASSWORD<input type="password"> + eye toggleplaceholder — the built-in toggle flips the input to plain text and back; the value never leaves the field
NUMBER<input type="number">min, max, step
CURRENCY<input type="number">min, max, step (use "0.01")
PERCENT<input type="number">min, max, step
DATE<input type="date">min, max, stepyyyy-MM-dd
DATETIME<input type="datetime-local">min, max, stepyyyy-MM-ddTHH:mm
BOOLEAN<input type="checkbox">value (truthy = checked)
SELECT<select>options
MULTISELECT<select multiple>options; value may be a list or a comma-separated string
FILE<input type="file">accept, multiple
REFERENCE<input type="text">placeholder, icon — a semantic marker for a foreign-key value; the renderer treats it like TEXT

PASSWORD builds the reveal toggle in: the eye button (wired by the event bus) switches the input between password and text, so an admin can check what is in the field before saving. Factory: UiField.password(id, label, value).

icon, submitOnChange and onChange work with every type. CURRENCY and PERCENT are semantic labels only: the renderer emits the same number input as NUMBER, so format the display value yourself for the read-only case.

Building one

// The factories pick the fieldType; the fluent setters do the rest.
UiField.text("name", "Name", null).asRequired().asEditable()
.placeholder("e.g. Widget").hint("Shown in the product list.");

UiField.textarea("desc", "Description", null).asEditable().submitOnEnter();

UiField.number("price", "Price", 19.0).asEditable().min("0").step("0.01");

UiField.date("launch", "Launch date", "2026-07-06").asEditable()
.range("2026-01-01", "2026-12-31");

UiField.select("category", "Category", "tools", List.of(
UiField.Option.of("tools", "Tools"),
UiField.Option.of("toys", "Toys"))).asEditable();

UiField.multiselect("tags", "Tags", null, List.of(
UiField.Option.of("new", "New"),
UiField.Option.of("sale", "Sale"))).asEditable();

UiField.bool("active", "Active", true).asEditable()
.onChange(UiTrigger.invoke("toggleActive"));

UiField.file("avatar", "Avatar").accept("image/*").multiple()
.onChange(UiTrigger.upload("/api/avatar"));

UiField.reference("owner", "Owner", "u-42").asEditable();

// Read-only, and rejected-submit rendering:
UiField.text("sku", "SKU", "WGT-0042"); // editable = false
UiField.text("name", "Name", "").asEditable().error("Name is required.");
UiField.text("q", "Search", null).asEditable().editableIf(canEdit);

Notes

id is the payload key. The name attribute keeps the model id verbatim, so {"name": "Widget", "price": "19.0"} falls out of a form whose fields are name and price. The DOM id of the control is suffixed with __input so the wrapper can keep the canonical id for patch targeting — patch the field id, not the input id.

editable defaults to false. This trips people up: a field built with UiField.text(...) and dropped into a form renders as static text and never reaches the server. Call .asEditable() (or .editableIf(condition)) on every input you actually want filled in. UiField.file(...) is the one factory that sets it for you.

required is a marker, not validation. It adds the asterisk and nothing else — no required attribute, no client-side check. Validate on the server and send the message back in validationError, with a summary in the form's formError.

onChange beats submitOnChange for partial updates. submitOnChange posts the entire form; onChange dispatches its own trigger with the form's values folded in as payload, so one field can drive a PATCH (no round-trip) or an INVOKE handler. Both markers can be present — the bus prefers the trigger.

File fields versus the drop zone. A FILE field is a single inline picker; pair it with UiTrigger.upload(url) on onChange to send the files as multipart/form-data. When you want a drop target with a prompt and a hint, reach for upload instead.

See also

  • form — the container that submits these fields.
  • fieldgroup — a titled <fieldset> around a set of fields.
  • upload — drag-and-drop alternative to a FILE field.
  • Forms — layout, validation and round-trip patterns.
  • Triggers & actions — what an onChange can do.