Sortable Tables
A table is a column of rows whose cells share their borders,
which border_collapse draws once between neighbors,
as CSS's border-collapse: collapse does.
To sort it by the header clicked, keep which column it's sorted by, and which way, in state,
and sort the rows by it each render:
@dataclass(frozen=True, slots=True)
class Peak:
name: str
height: int
first_ascent: int
PEAKS = (
Peak("Everest", 8849, 1953),
Peak("K2", 8611, 1954),
Peak("Kangchenjunga", 8586, 1955),
Peak("Lhotse", 8516, 1956),
Peak("Makalu", 8485, 1955),
Peak("Cho Oyu", 8188, 1954),
Peak("Annapurna I", 8091, 1950),
)
@dataclass(frozen=True, slots=True)
class Column:
label: str
width: int
value: Callable[[Peak], str | int]
COLUMNS = (
Column("Peak", 15, lambda peak: peak.name),
Column("Height (m)", 12, lambda peak: peak.height),
Column("First ascent", 14, lambda peak: peak.first_ascent),
)
@dataclass(frozen=True, slots=True)
class Sort:
column: int
is_descending: bool
def by(self, column: int) -> Sort:
"""Sorted by `column`, and the other way if it's already sorted by it."""
return Sort(column, not self.is_descending if column == self.column else False)
CELL = content_box | border | pad_x(1)
@component
def peak_table() -> Div:
sort, set_sort = use_state(Sort(column=1, is_descending=True))
def sort_by(column: int) -> None:
set_sort(lambda sort: sort.by(column))
arrow = "↓" if sort.is_descending else "↑"
header = Div(
style=row | border_collapse,
children=tuple(
Text(
content=f"{column.label} {arrow if index == sort.column else ' '}",
style=CELL | width(column.width) | text_color("amber", 300) | text_bold,
on_input=on_click(partial(sort_by, index)),
)
for index, column in enumerate(COLUMNS)
),
)
peaks = sorted(PEAKS, key=COLUMNS[sort.column].value, reverse=sort.is_descending)
rows = tuple(
Div(
style=row | border_collapse,
children=tuple(
Text(content=str(column.value(peak)), style=CELL | width(column.width) | text_color("slate", 300))
for column in COLUMNS
),
)
for peak in peaks
)
return Div(style=col | border_collapse, children=(header, *rows))
┌─────────────────┬──────────────┬────────────────┐
│ Peak │ Height (m) │ First ascent ↑ │
├─────────────────┼──────────────┼────────────────┤
│ Annapurna I │ 8091 │ 1950 │
├─────────────────┼──────────────┼────────────────┤
│ Everest │ 8849 │ 1953 │
├─────────────────┼──────────────┼────────────────┤
│ K2 │ 8611 │ 1954 │
├─────────────────┼──────────────┼────────────────┤
│ Cho Oyu │ 8188 │ 1954 │
├─────────────────┼──────────────┼────────────────┤
│ Kangchenjunga │ 8586 │ 1955 │
├─────────────────┼──────────────┼────────────────┤
│ Makalu │ 8485 │ 1955 │
├─────────────────┼──────────────┼────────────────┤
│ Lhotse │ 8516 │ 1956 │
└─────────────────┴──────────────┴────────────────┘
Clicking a header sorts by its column, and clicking it again sorts the other way:
Each cell has the width of its column, so the cells of a column line up from row to row.
Sort.by is a function of the latest sort, passed to the setter,
so two clicks that arrive together sort by the second one the way the first one left it.