Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

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

DataGrid(…, filterable: true, selectable: true) { slot selection { picked => Button("Archive", onPress: () => Archive(picked)); } }

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.

stable1 example compiled by CIuikitdatagrid

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?#

towrite
open the filter rowUi.Click("Filter") · or a column's: Ui.Click("Filter by Client")
type into a column's filterUi.Fill("Filter Total", "> 1000")
assert the row is shutAssert.Expanded("Filter", false)
remove a chipUi.Click("Remove filter Total")
pick a rowUi.Click("Select F-1031") — the row's title column names its check
pick every row in viewUi.Click("Select all rows")
act on the pickUi.Click("Mark as sent", within: "Selection") · Assert.Visible("2 selected")

See also#

Related

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…

Sorting by a column the user picks

A sortable table names its sort key with the chosen column's own selector, never with a string. Over rows already…

Cell template (your own content in a control's cell)

A control that paints cells — a grid — writes plain text in each one. A `slot <Field> { row => … }` block on the call…

Dock

The page's drawers: the page in the middle, a drawer docked on the right (the record a row opened, an inspector) and…

Arrangements — five ways to build a whole app, and which look wears each

A look decides how an app's pixels read; an ARRANGEMENT decides how the app is built — where navigation lives, how a…