# grid_state

URL: https://lbliii.github.io/chirp-ui/api/grid_state/
Section: api
Description: Server-driven data-grid state helpers for chirp-ui (issue #200).

Deliberately modeled on `chirp_ui.route_tabs`: stdlib + dataclasses only,
no ``import chirp`` and no ``import kida``. The module is fully unit-testable
with plain pytest and ``ty``-checkable without a render or a Chirp app
("works without Chirp, better with Chirp").

Two concerns, two analogs of ``route_tabs.tab_is_active``:

* **Sort.** `parse_sort`() turns a raw ``?sort=`` query value into a typed
  `GridSort`; `sort_columns`() projects a list of `Column`
  declarations into `ColumnSort` rows that carry the ``aria_sort`` value
  and the fully-built toggle ``next_url`` the macro renders **but never
  computes**. The same `GridSort` the route uses to actually order rows
  produces the rendered ``aria-sort`` and next-request, so server data and
  advertised UI cannot drift.

* **Selection.** `selection_state`() normalizes request-derived ids into a
  `SelectionState` whose 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 ``resizable`` are 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())

---

> For a complete page index, fetch https://lbliii.github.io/chirp-ui/llms.txt.

Server-driven data-grid state helpers for chirp-ui (issue #200).

Deliberately modeled on`chirp_ui.route_tabs`: stdlib + dataclasses only,
no`import chirp` and no `import kida`. The module is fully unit-testable
with plain pytest and`ty`-checkable without a render or a Chirp app
("works without Chirp, better with Chirp").

Two concerns, two analogs of`route_tabs.tab_is_active`:

-

Sort.`parse_sort`() turns a raw `?sort=`query value into a typed
`GridSort`; `sort_columns`() projects a list of `Column`
declarations into`ColumnSort` rows that carry the `aria_sort`value
and the fully-built toggle`next_url`the macro renders but never
computes. The same`GridSort`the route uses to actually order rows
produces the rendered`aria-sort`and next-request, so server data and
advertised UI cannot drift.

-

Selection.`selection_state`() normalizes request-derived ids into a
`SelectionState` whose 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 `resizable`are 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

class
Column

A column declaration — the developer-authored input to the grid.

`key` is the stable sort key sent to the server (never `label|lower`);
renaming …

Jump to symbol

(#Column)

class
GridSort

The typed current-sort state.`direction` is `"asc"` or `"desc"`.

Jump to symbol

(#GridSort)

class
ColumnSort

A projected column ready to render.

The macro reads`aria_sort` and `next_url`directly; it never
derives sort state.`aria_sort` is one of `"ascending"`,
…

Jump to symbol

(#ColumnSort)

class
SelectionState

Server-authoritative selection snapshot for the current page.

`selected` is the full cross-request selection; `page_ids`are the ids
rendered on this page;`total`is the…

Jump to symbol

(#SelectionState)

function
parse_sort

Turn a raw`?sort=` value into a typed `GridSort`.

`"name"` → ascending, `"-name"`→ descending. Unknown or empty keys
clamp to`default_key`—…

Jump to symbol

(#parse_sort)

function
column_aria_sort

Return the`aria-sort` value for `column_key` given `sort`.

Standalone projection primitive for callers who hand-render a single
`<th>`. Returns `"ascending"` / `"descending"…`

Jump to symbol

(#column_aria_sort)

function
sort_query

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 …

Jump to symbol

(#sort_query)

function
sort_columns

Project`columns` into renderable `ColumnSort`rows.

The`tab_is_active`() analog for tables. For each column it
computes`is_active` (`col.sortable and col.key == sort.key…`

Jump to symbol

(#sort_columns)

function
selection_state

Normalize request-derived selection into a`SelectionState`.

`selected_ids` (e.g. `req.query.getlist("ids")`) is coerced to a
`frozenset` of strings; tolerant of `None`. `page_ids`are…

Jump to symbol

(#selection_state)

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`);
renaming`label`or shipping duplicate/i18n labels never breaks sorting.

v1 does not expose per-column pinning: the grid pins the first visual
column via`sticky_first_col=true` (a CSS `:first-child`rule), not an
arbitrary column. A`frozen`flag was deliberately not shipped so the
public surface advertises no pinning contract the renderer cannot honor.

`width`, `mobile_width`, and `resizable`seed 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 reads`aria_sort` and `next_url`directly; it never
derives sort state.`aria_sort` is one of `"ascending"`,
`"descending"`, `"none"`; `next_url`is the fully-built toggle URL.

SelectionState

class

Server-authoritative selection snapshot for the current page.

`selected` is the full cross-request selection; `page_ids`are the ids
rendered on this page;`total` is the grand result-set size (or `None`).
The`all_selected` / `partial`props 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 to`default_key` — the `tab_is_active`() empty-href
guard analog, defensive against arbitrary query input. When`allowed`is
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 the`aria-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 requests`key`(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]
```

Project`columns` into renderable `ColumnSort`rows.

The`tab_is_active`() analog for tables. For each column it
computes`is_active` (`col.sortable and col.key == sort.key`),
`aria_sort` (`ascending` / `descending`when active+sortable, else
`none`) and the toggled `next_url` (active+asc → request `-key`;
active+desc → request`key`; inactive → request `key`ascending),
preserving`extra_params`so an active filter query survives a sort click.

Exactly one column is marked active for a given`GridSort`, so the
single-sort + single`aria-sort`invariant 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 a`SelectionState`.

`selected_ids` (e.g. `req.query.getlist("ids")`) is coerced to a
`frozenset` of strings; tolerant of `None`. `page_ids`are the ids
rendered on the current page.`total`is 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
(https://github.com/lbliii/chirp-ui/blob/main//home/runner/work/chirp-ui/chirp-ui/site/../src/chirp_ui/grid_state.py#L1)
