Skip to main content

section-entry — one tab and its panel

UiSectionEntry is a single tab inside a section: the tab label and icon on one side, the content node that becomes the panel body on the other. It is a real UiNode with its own type discriminator, which is why the editor can select, edit and delete a tab with exactly the same code path as any other node.

You rarely construct one by hand — UiSection.section(...) builds them for you — but the entry is where icon, href and onClick live.

Live — an entry has no standalone screen presence, so this is a small section whose three entries each show one thing an entry carries: title only, title plus icon, and an entry with an onClick trigger that fires alongside the switch.

Fields

FieldTypeMeaning
idStringTab id — the panel's DOM id, the value initialSection matches, and the patch target for the panel.
titleStringThe tab label.
contentUiNodeThe body shown when this tab is active. Any node.
iconStringIcon token rendered before the tab title. See icons.
hrefStringWhen set, the tab renders as an <a href> navigation link instead of a JS-driven <button>. Null = client-side panel switch.
onClickUiTriggerTrigger fired in addition to activating this panel. Independent of href.
selectOnClickbooleanDefault true. false fires onClick but leaves the panel where it is — the application decides. See Guarding a tab.
cssClassStringExtra CSS class on the entry wrapper.

When the editor renders an entry on its own it emits a labelled box — the title as an <h3> above the content — enough to read the structure without the surrounding tab bar.

Building one

// usually built through the section:
UiSection.of("product-tabs", null)
.section("overview", "Overview", overviewBody)
.section("audit", "Audit", "/products/42/audit", auditList); // href variant

// directly, when you need icon or onClick:
UiSectionEntry.of("stock", "Stock", stockTable)
.icon("grid");

UiSectionEntry.of("history", "History", placeholder)
.icon("document")
.onClick(UiTrigger.api("GET", "/products/42/history"));

Notes

onClick fires after the switch. By default the panel changes first and the trigger runs afterwards, so it can react but never prevent. To decide whether the tab may change, set selectOnClick: false — see below.

onClick is the lazy-load hook. The panel still switches; the trigger fires alongside it. Ship a spinner or a placeholder as content, fetch the real body on first open and patch it into the panel by the entry's id.

href is for SSR, onClick is for behaviour. href says "this tab is a different URL" — the tab becomes an anchor that works with JavaScript off, and the SPA intercepts it and routes through navigate(). They are independent: an entry can have both.

Title-less entries turn off the tab bar. If every entry in a section has no title and no href, the section renders all panels visible instead of empty tab buttons. Handy deliberately; a surprise if you forget a title on one entry of a set — then you get one blank tab.

The entry id owns the panel. Patches that replace a tab's body target the entry's id, not the section's. Keep them stable across renders.

Guarding a tab

Sometimes a tab must not change on click: unsaved edits, a permission check, a confirmation. selectOnClick: false keeps the panel where it is and hands the decision to your handler, which selects a tab by patching the section's initialSection.

Live — "Billing" asks first. Decline and the panel never moves; allow and the handler switches it by patch.

UiSectionEntry.of("billing", "Billing", billingPanel())
.selectOnClick(false) // don't switch on click
.onClick(UiTrigger.api("GET", "/billing/may-open"));

The endpoint decides. To let the tab through, return a patch that re-renders the section with the new initialSection:

return UiPatch.of().patch(UiPatch.Operation.replace("sec",
sectionWith("billing"))); // same tree, different active tab
Why a patch rather than a "select" call

The active tab is not hidden state — it is initialSection on the section node. Re-rendering the section with a different value is selecting a tab, and the morpher keeps the surrounding DOM (and any focus or scroll) intact. One mechanism, no separate imperative API to learn.

See also