# Acting on data — a calm grid header, filters as chips, and a toolbar that follows the selection

> A DataGrid reads calmly: its per-column filter row is shut until somebody asks to filter (the Filter toggle, or a column's funnel), a filter that is set stays in view as a removable chip, and the sort mark shows only on the sorted column. With selectable: true each row gets a check, and while anything is picked the toolbar becomes the selection's — its count, the app's own verbs over exactly the picked rows, and Clear.

<!-- id: ui-selection · area: ui · stability: stable · html: https://osysharp.com/reference/ui/selection/ -->

## Summary        {#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.

```osy title="a grid whose toolbar acts on what is picked" test app=ui-selection
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      {#signature}

```osy syntax
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    {#description}

### When does the filter row show?   {#filter-row}

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?   {#chips}

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?   {#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?   {#pinned-filter}

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?   {#selection}

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?   {#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       {#see-also}

- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the DataGrid's columns, cell templates and views
- [Cell template (your own content in a control's cell)](https://osysharp.com/reference/ui/cell-template/) — rendering a cell as a badge or a control
- [Dock](https://osysharp.com/reference/ui/dock/) — opening the picked row beside the table
- [Arrangements — five ways to build a whole app, and which look wears each](https://osysharp.com/reference/ui/arrangements/) — the archetypes that put a selection toolbar to work
