Summary#
Reading a table is the common case, so a DataGrid at rest shows its columns' names and nothing else: no row of
filter boxes, no contains… or = > < hints, an arrow only on the column the rows are sorted by. Filtering is asked
for — the toolbar's Filter opens the row of boxes, and a column's funnel opens it with that column's box focused —
and what is set stays visible as a chip ("Annual value over 40000 ×") once the row is shut, so nothing hidden is
forgotten.
Picking is the other half. With selectable: true each row leads with a check (and the header with one for every row
in view); while anything is picked the toolbar turns into the selection's: how many, the verbs the app gives it in
slot selection, and Clear.
class Invoice { public string Number; public string Client; public decimal Total; public bool Sent; }
List<Invoice> Invoices() {
return [
new Invoice { Number = "F-1031", Client = "Larkspur Studio", Total = 1840m },
new Invoice { Number = "F-1032", Client = "Wren & Pike", Total = 960m },
new Invoice { Number = "F-1033", Client = "Larkspur Studio", Total = 2210m },
];
}
[Route("/invoices")] [Render(CSR)] [AllowAnonymous]
component InvoicesPage() {
string said = "";
action MarkSent(Invoice[] picked) { said = picked.Length.ToString() + " invoices marked as sent."; }
render {
Stack(gap: 3, p: 6) {
if (said != "") { Alert(said, tone: Tone.Success); }
DataGrid(
label: "Invoices",
rows: Invoices(),
filterable: true,
selectable: true,
columns: [
new GridColumn<Invoice> { Name = "Number", Label = "Invoice", Value = i => i.Number, Title = true, Width = 120 },
new GridColumn<Invoice> { Name = "Client", Label = "Client", Value = i => i.Client, Fill = true },
new GridColumn<Invoice> { Name = "Total", Label = "Total", Kind = GridColumnKind.Number, Sort = UiSort.Descending,
Value = i => i.Total.ToString("N2"), Number = i => i.Total, Align = "right", Width = 120 },
]) {
slot selection { picked => Button("Mark as sent", onPress: () => MarkSent(picked), size: Size.Sm); }
}
}
}
}Signature#
DataGrid<T>(string label, T[] rows, GridColumn<T>[] columns, …,
bool filterable = false, bool selectable = false, Action<T[]>? onSelectionChange = null)
{ slot selection { picked => … } }
new GridColumn<T> { Name = …, Value = r => …, Sort = UiSort.Descending } // the column the rows arrive sorted by
SelectionBar(int count, Action onClear) { <the verbs> }
FilterChip(string label, string removeLabel, Action onRemove)
SearchField(string label, Binding<string> value)Description#
When does the filter row show?#
Only while somebody is filtering. The toolbar's Filter opens and shuts it (it counts the filters that are set:
"Filter · 2"); a column header's funnel — "Filter by Client", shown on hover and on keyboard focus like the header's
other tools — opens it with that column's box already focused. A number column's box takes a leading operator
(> 100, < 50, = 9); a text column's matches what it contains.
How do I see a filter that is set once the row is shut?#
As a chip under the toolbar, in words — "Total over 1000", "Client contains Lark" — with a × that removes it and "Clear all" beside them. The filtered column is also marked beside its name, in the accent. On the narrow arm the chips stay, so a filter set on a wide screen never silently narrows a phone's list.
Where is the sort mark?#
On the column the rows are sorted by — always, as ↑ or ↓ — and on any other column only while the pointer or the
keyboard is on its header, as ↕. Sort = UiSort.Descending (or Ascending) on a COLUMN sets the order the rows ARRIVE
in; the reader re-sorts from there. It is said on the column itself, so there is no name to misspell — the
sortColumn: "Total" string it replaced sorted silently by the first column when the name matched nothing. The first
column that says Sort wins. With none, the grid starts on its first column — unless that column says
Sortable = false: then the rows stand in the order the page handed them, and no header shows a mark, until the reader
sorts by a column that can be. That is how a list the page already ordered (most urgent first, a tree) keeps its order.
How does a page pin a filter the reader cannot widen?#
By passing the rows it means: rows: invoices.Where(i => i.Total > 1000). The predicate is ordinary typed Osy#, checked
against the row type. (The grid's old filters: [new GridFilter { Column = "Total", … }] named its column by a string
and is gone.) The reader's own filters — the row, the chips — then narrow what the page passed.
What does a selection act on?#
Exactly the rows that are picked, handed to the slot as picked — a T[]. Rows are picked by identity, so a pick
survives sorting and filtering; the header's check picks every row in VIEW (what a search hid stays unpicked), or, when
every row in view is already picked, clears them. onSelectionChange hands the pick to the page whenever it changes,
for a page that needs it outside the grid.
SelectionBar is the same toolbar on its own — for a board, a card list or an inbox that keeps its own pick.
How do I drive it in a test?#
| to | write |
|---|---|
| open the filter row | Ui.Click("Filter") · or a column's: Ui.Click("Filter by Client") |
| type into a column's filter | Ui.Fill("Filter Total", "> 1000") |
| assert the row is shut | Assert.Expanded("Filter", false) |
| remove a chip | Ui.Click("Remove filter Total") |
| pick a row | Ui.Click("Select F-1031") — the row's title column names its check |
| pick every row in view | Ui.Click("Select all rows") |
| act on the pick | Ui.Click("Mark as sent", within: "Selection") · Assert.Visible("2 selected") |
See also#
- control — foreign UI controls (charts, grids, maps) — the DataGrid's columns, cell templates and views
- Cell template (your own content in a control's cell) — rendering a cell as a badge or a control
- Dock — opening the picked row beside the table
- Arrangements — five ways to build a whole app, and which look wears each — the archetypes that put a selection toolbar to work