action — buttons and links
UiAction is anything clickable: a button, a link, an icon-only control. It
answers two independent questions — what it is in the DOM (appearance) and
what colour it wears (style) — and carries the click behaviour in a single
onClick trigger.
Live — every button here is one UiAction. "Delete" asks for confirmation
first; "Publish" is disabled with a reason; "Importing…" is busy by model state.
Fields
| Field | Type | Meaning |
|---|---|---|
id | String | Node id — also the DOM id and the patch target. |
label | String | Button text. For appearance: ICON it becomes the accessible name. |
style | PRIMARY · SECONDARY · DANGER | Colour scheme. Ignored by LINK. |
appearance | BUTTON · LINK · ICON | DOM shape. Defaults to BUTTON when absent. |
icon | String | Icon token rendered before the label — or as the control for ICON. See icons. |
enabled | boolean | Defaults to true. false renders disabled. |
disabledReason | String | Tooltip explaining why it's disabled. |
confirm | String | Confirmation text. The bus asks before firing onClick. |
loading | boolean | Declarative busy state: spinner + disabled. |
onClick | UiTrigger | What happens on click. null = inert. |
cssClass | String | Extra CSS class on the element. |
Building one
- Java
- JSON
// The factories set style + appearance together:
UiAction.primary("save", "Save")
.onClick(UiTrigger.api("POST", "/products", "product-form"));
UiAction.secondary("cancel", "Cancel")
.onClick(UiTrigger.go("/products"));
UiAction.danger("delete", "Delete")
.confirm("Delete this product?")
.onClick(UiTrigger.api("DELETE", "/products/42"));
// A link, an icon button, a disabled button:
UiAction.link("details", "View details").onClick(UiTrigger.go("/products/42"));
UiAction.secondary("edit", "Edit").icon("edit").appearance(UiAction.Appearance.ICON);
UiAction.primary("publish", "Publish").disabled("Fill in the required fields first");
{ "type": "action", "id": "save", "label": "Save", "style": "PRIMARY",
"onClick": { "behavior": "APPLY_RESPONSE", "method": "POST",
"url": "/products", "payload": "product-form" } }
{ "type": "action", "id": "delete", "label": "Delete", "style": "DANGER",
"confirm": "Delete this product?",
"onClick": { "behavior": "APPLY_RESPONSE", "method": "DELETE", "url": "/products/42" } }
{ "type": "action", "id": "edit", "label": "Edit", "icon": "edit",
"appearance": "ICON", "style": "SECONDARY",
"onClick": { "behavior": "APPLY_RESPONSE", "method": "GET", "url": "/products/42/edit" } }
Notes
confirm costs nothing. The event bus intercepts the click and asks before
dispatching — no dialog node, no handler, no state. Reach for
dialog only when the overlay needs content.
enabled: false deserves a reason. disabledReason becomes the tooltip; a
disabled control with no explanation is a dead end for the user.
loading vs. automatic busy state. The bus already marks the clicked element
busy for the duration of its own request — leave loading alone for that. Set it
explicitly only when the server owns the state and pushes loading: true in a
patch.
The URL need not be a string. On Spring MVC, UiActions.trigger(on(X.class) .method(args)) derives an onClick's path and verb from the handler itself, so
renaming it updates the button and a wrong argument list fails the build. See
Naming the handler instead of the URL.
Actions live in containers. form, table
(actions and per-row rowActions), detail and
list items all take a list of actions — that's where most of them
belong, rather than loose in a stack.
See also
- Triggers & actions — every behaviour an
onClickcan have. - Triggers cookbook — working recipes.
- Naming the handler instead of the URL — derive the trigger from a Spring MVC method.
link— plain navigation without button semantics.menu-button— several actions behind one control.