Skip to content

Datagrid columns

The operations layer every line-of-business grid grows first: per-user column choice. This recipe keeps it hypermedia — a filter-popover shell holds one checkbox per column, Apply GETs the grid URL with repeated cols= params, and the server re-renders the whole datagrid with only those columns. No client column hiding: the server deciding which columns exist means one round trip, zero state drift, and print/export match the screen for free. Zero new JavaScript. Covered by the versioning policy.

Open Columns, uncheck a few, Apply — the server answers the grid with exactly those columns (in the submitted order) plus the chooser re-rendered out of band with matching checked states. Drag a row’s ⠿ handle to reorder before applying — the submitted cols= sequence becomes the column order (the datagrid-prefs upgrade). Uncheck everything to see the absent-params branch bring the default set back.

<button class="hc-button" type="button" popovertarget="cols-popover">
Columns
</button>
<div id="cols-popover" class="hc-popover" popover
data-side="bottom" data-align="start">
<form id="cols-chooser" action="/items" method="get"
data-hx-get="/items"
data-hx-target="#items-grid"
data-hc-close-popover-on-success>
<fieldset class="hc-popover__body" id="cols-fields">
<label class="hc-checkbox-label">
<input class="hc-checkbox" type="checkbox" name="cols" value="name" checked>
Name
</label>
<!-- …one checkbox per column: status, owner, updated… -->
</fieldset>
<footer class="hc-popover__footer">
<button class="hc-button" type="submit" data-variant="primary">Apply</button>
</footer>
</form>
</div>
<div class="hc-datagrid" id="items-grid">
<!-- server-rendered: .hc-datagrid__scroll > table with the current columns -->
</div>

The chooser is the filter-popover shell — data-hc-close-popover-on-success (installClosePopover(), auto-installed) dismisses it on any 2xx. Every checkbox shares name="cols", so Apply serializes repeated cols= params, and the form targets the grid wrapper (innerHTML default): header and rows always change together. The chooser’s fieldset keeps a stable id — it is the out-of-band swap anchor for the re-rendered checked states (never the form; see the contract below).

GET /items?cols=name&cols=status&…:

CaseResponse (200)
any cols setthe grid fragment with exactly those columns, in the submitted order (the datagrid-prefs upgrade), plus an OOB outerHTML re-render of the chooser fieldset with matching checked states (never the form — it carries data-hc-close-popover-on-success and must survive the request)
empty/absent colsthe server’s default column set — an all-unchecked Apply serializes no cols at all, so it lands here
unknown col nameignored (the server is the schema); nothing recognized falls back to the default set

The requested set wins — and, since the datagrid-prefs upgrade, so does the submitted sequence (a sortable chooser serializes its DOM order). Persisting the choice per user is the server’s option. A CSV export needs no recipe either: <a href="/items.csv?cols=…"> reuses the same params, so the file matches the screen.

The chooser is a real GET form (action + method="get"): without JavaScript, Apply navigates to /items?cols=… and the server renders the full page with those columns. The grid itself is server-rendered HTML — column choice never depends on client JavaScript.

  • Native checkboxes with visible labels (hc-checkbox-label) — the chooser is a plain form; no menu semantics to fake.
  • Every response keeps the grid a real <table> with scope="col" headers, so a removed column is removed for assistive tech too — which is the point.
  • Closing the popover on success returns focus to the trigger button (native popover behavior).