Server-driven data-grid state helpers for chirp-ui (issue #200).
Deliberately modeled onchirp_ui.route_tabs: stdlib + dataclasses only,
noimport chirp and no import kida. The module is fully unit-testable
with plain pytest andty-checkable without a render or a Chirp app
("works without Chirp, better with Chirp").
Two concerns, two analogs ofroute_tabs.tab_is_active:
-
Sort.
parse_sort() turns a raw?sort=query value into a typedGridSort;sort_columns() projects a list ofColumndeclarations intoColumnSortrows that carry thearia_sortvalue and the fully-built togglenext_urlthe macro renders but never computes. The sameGridSortthe route uses to actually order rows produces the renderedaria-sortand next-request, so server data and advertised UI cannot drift. -
Selection.
selection_state() normalizes request-derived ids into aSelectionStatewhose props (count,all_selected,none_selected,partial) seed the select-all checkbox server-side, so selection is correct on a full load even with JavaScript off. The Alpine factory owns live in-page toggling between requests.
Example (Chirp route)::
from chirp_ui import Column, parse_sort, sort_columns, selection_state
COLS = [
Column("name", "Name", sortable=True),
Column("status", "Status", sortable=True, align="center"),
Column("seats", "Seats", sortable=True, align="right",
width="1fr", mobile_width="80px", resizable=True),
]
width, mobile_width, and resizableare layout hints for the future
opt-in ARIA-grid renderer (issue #261). Table mode ignores them today.
sort = parse_sort(req.query.get("sort"), default_key="name",
allowed=tuple(c.key for c in COLS))
rows = query_users(order_by=sort.key, desc=(sort.direction == "desc"))
cols = sort_columns(COLS, sort, base_url="/users",
extra_params={"q": req.query.get("q", "")})
sel = selection_state(req.query.getlist("ids"),
page_ids=[u.id for u in rows], total=count_users())
grid_state
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
A column declaration — the developer-authored input to the grid.
key is the stable sort key sent to the server (never label|lower);
renaming …
The typed current-sort state.direction is "asc" or "desc".
A projected column ready to render.
The macro readsaria_sort and next_urldirectly; it never
derives sort state.aria_sort is one of "ascending",
…
Server-authoritative selection snapshot for the current page.
selected is the full cross-request selection; page_idsare the ids
rendered on this page;totalis the…
Turn a raw?sort= value into a typed GridSort.
"name" → ascending, "-name"→ descending. Unknown or empty keys
clamp todefault_key—…
Return thearia-sort value for column_key given sort.
Standalone projection primitive for callers who hand-render a single
<th>. Returns "ascending" / "descending"…
Return the?param=value query string that toggles column_key.
Standalone projection primitive. An active ascending column requests
-key(descending); an active descending column requests …
Projectcolumns into renderable ColumnSortrows.
Thetab_is_active() analog for tables. For each column it
computesis_active (col.sortable and col.key == sort.key…
Normalize request-derived selection into aSelectionState.
selected_ids (e.g. req.query.getlist("ids")) is coerced to a
frozenset of strings; tolerant of None. page_idsare…
Column
class
A column declaration — the developer-authored input to the grid.
key is the stable sort key sent to the server (never label|lower);
renaminglabelor shipping duplicate/i18n labels never breaks sorting.
v1 does not expose per-column pinning: the grid pins the first visual
column viasticky_first_col=true (a CSS :first-childrule), not an
arbitrary column. Afrozenflag was deliberately not shipped so the
public surface advertises no pinning contract the renderer cannot honor.
width, mobile_width, and resizableseed the ARIA-grid-over-div
variant (issue #261). Real-<table>mode ignores them — callers can set
them now without changing table rendering.
GridSort
class
The typed current-sort state.direction is "asc" or "desc".
ColumnSort
class
A projected column ready to render.
The macro readsaria_sort and next_urldirectly; it never
derives sort state.aria_sort is one of "ascending",
"descending", "none"; next_urlis the fully-built toggle URL.
SelectionState
class
Server-authoritative selection snapshot for the current page.
selected is the full cross-request selection; page_idsare the ids
rendered on this page;total is the grand result-set size (or None).
Theall_selected / partialprops are page-scoped — they describe
the visible page, not the entire result set (cross-page "select all N
matching" is out of scope for v1).
parse_sort
function
def parse_sort(raw: str | None, *, default_key: str = '', default_direction: str = _ASC, allowed: Sequence[str] = ()) -> GridSort
Turn a raw?sort= value into a typed GridSort.
"name" → ascending, "-name"→ descending. Unknown or empty keys
clamp todefault_key — the tab_is_active() empty-href
guard analog, defensive against arbitrary query input. Whenallowedis
given, a key outside it also clamps to the default.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
raw
|
str | None
|
— | |
default_key
|
str
|
''
|
|
default_direction
|
str
|
_ASC
|
|
allowed
|
Sequence[str]
|
()
|
column_aria_sort
function
def column_aria_sort(column_key: str, sort: GridSort) -> str
Return thearia-sort value for column_key given sort.
Standalone projection primitive for callers who hand-render a single
<th>. Returns "ascending" / "descending"when this column is the
active sort, else"none".
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
column_key
|
str
|
— | |
sort
|
GridSort
|
— |
sort_query
function
def sort_query(column_key: str, sort: GridSort, param: str = 'sort') -> str
Return the?param=value query string that toggles column_key.
Standalone projection primitive. An active ascending column requests
-key (descending); an active descending column requests key
(ascending); an inactive column requestskey(ascending).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
column_key
|
str
|
— | |
sort
|
GridSort
|
— | |
param
|
str
|
'sort'
|
sort_columns
function
def sort_columns(columns: Sequence[Column | Mapping[str, object]], sort: GridSort, base_url: str, *, param: str = 'sort', extra_params: Mapping[str, str] | None = None) -> list[ColumnSort]
Projectcolumns into renderable ColumnSortrows.
Thetab_is_active() analog for tables. For each column it
computesis_active (col.sortable and col.key == sort.key),
aria_sort (ascending / descendingwhen active+sortable, else
none) and the toggled next_url (active+asc → request -key;
active+desc → requestkey; inactive → request keyascending),
preservingextra_paramsso an active filter query survives a sort click.
Exactly one column is marked active for a givenGridSort, so the
single-sort + singlearia-sortinvariant is structural, not asserted in
a template branch.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
columns
|
Sequence[Column | Mapping[str, object]]
|
— | |
sort
|
GridSort
|
— | |
base_url
|
str
|
— | |
param
|
str
|
'sort'
|
|
extra_params
|
Mapping[str, str] | None
|
None
|
selection_state
function
def selection_state(selected_ids: Iterable[str] | None, page_ids: Iterable[str], total: int | None = None) -> SelectionState
Normalize request-derived selection into aSelectionState.
selected_ids (e.g. req.query.getlist("ids")) is coerced to a
frozenset of strings; tolerant of None. page_idsare the ids
rendered on the current page.totalis the grand result-set size.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
selected_ids
|
Iterable[str] | None
|
— | |
page_ids
|
Iterable[str]
|
— | |
total
|
int | None
|
None
|
View source · /home/runner/work/chirp-ui/chirp-ui/site/../src/chirp_ui/grid_state.py:1