Skip to main content

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

FieldTypeMeaning
idStringNode id — also the DOM id and the patch target.
labelStringButton text. For appearance: ICON it becomes the accessible name.
stylePRIMARY · SECONDARY · DANGERColour scheme. Ignored by LINK.
appearanceBUTTON · LINK · ICONDOM shape. Defaults to BUTTON when absent.
iconStringIcon token rendered before the label — or as the control for ICON. See icons.
enabledbooleanDefaults to true. false renders disabled.
disabledReasonStringTooltip explaining why it's disabled.
confirmStringConfirmation text. The bus asks before firing onClick.
loadingbooleanDeclarative busy state: spinner + disabled.
onClickUiTriggerWhat happens on click. null = inert.
cssClassStringExtra CSS class on the element.

Building one

// 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");

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