bo-grid v3.1.0

Sorting & filtering

.md

Sorting & filtering

Sort by one or more columns, search across every column, and filter each column with a menu matched to its type.

Every column sorts from its header, and the grid has three ways to filter: a quick search across all columns, a row of per-column inputs, and a filter menu on each header. They combine: a row is shown when it passes all of them.

Click a header to sort, Shift-click another to add it to the sort, open a header’s funnel to filter it (Sector has a checkbox list), or type in the search box.

sorting-filtering.svelte GitHub ↗

Sorting

Click a column header to sort it ascending, again for descending, and a third time to clear. Shift-click more headers to sort by several columns; each sorted header shows its place in the order.

  • Numeric columns sort by number, dates by time, text with a locale-aware collator.
  • Give a column compare(a, b) for a custom order (a status workflow, a rating scale).
  • Set sortable: false to stop a column sorting. Sparkline columns never sort.

A sort you own

Sorting is uncontrolled by default. To set an initial sort, persist it or sync it to the URL, pass sort and handle onSortChange:

<script lang="ts">
  import { Grid, type SortState } from 'bo-grid';
  let sort = $state<SortState[]>([{ key: 'volume', dir: 'desc' }]);
</script>

<Grid {rows} {columns} height={480} {sort} onSortChange={(s) => (sort = s)} />

Sorting live data

A sort is a snapshot: when values change in place (a price feed), rows keep their position, so a live board never reshuffles under the reader. Call api.refresh() when the order should catch up. With api.patchRows, liveSort keeps it current instead: rows whose sorted or filtered field changed are merged back into place at most every 500 ms (or the interval you give). See Live updates.

Pass a filter string to keep only rows where some column’s value contains it, ignoring case. Bind it to your own search input, or set quickFilter for a built-in search box above the grid (with a server source, its text reaches getRows as quickFilter):

<script lang="ts">
  let q = $state('');
</script>

<input type="search" bind:value={q} placeholder="Search" />
<Grid {rows} {columns} height={480} filter={q} />

Filter row

filterRow adds a row of inputs under the header, one per column. A row must match every input that has text. In-memory data only.

<Grid {rows} {columns} height={480} filterRow />

Filter menu

filterMenu adds a funnel to each header. Its menu matches the column type:

ColumnOperators
Textcontains, does not contain, equals, starts with, ends with
Number=, ≠, <, ≤, >, ≥, between
Datebefore, after, on, between (whole days, in the viewer’s time zone)
Set (filter: 'set')a searchable checkbox list of the column’s distinct values

Set filter: false on a column to leave it without a funnel. The menu’s code loads on first open, so it costs nothing until it is used.

const columns: ColumnDef[] = [
  { type: 'text', key: 'sector', header: 'Sector', filter: 'set' },
  { type: 'price', key: 'price', header: 'Price' },          // number operators
  { type: 'text', key: 'note', header: 'Note', filter: false }, // no funnel
];

For a filter kind of your own (a band, a range rule), see Custom filters.

Filters you own

Like sorting, filtering is uncontrolled by default. Pass columnFilters (a map from column key to filter) and handle onFilterChange to set initial filters, save them or put them in the URL:

<script lang="ts">
  import { Grid, type AnyFilter } from 'bo-grid';
  let filters = $state<Record<string, AnyFilter>>({
    change: { kind: 'number', op: 'gt', a: 0 },
    sector: { kind: 'set', excluded: ['Energy'] },
  });
</script>

<Grid {rows} {columns} height={480} filterMenu columnFilters={filters} onFilterChange={(f) => (filters = f)} />

A filter is a plain, JSON-safe object: { kind: 'text', op, q }, { kind: 'number', op, a, b? }, { kind: 'date', op, a, b? } (epoch ms) or { kind: 'set', excluded }.

With a server source

With a source, the grid sends the sort, the filter text and the column filters to your getRows and shows what it returns. Text, number and date menus work as usual; a set filter needs the distinct values, so it is for in-memory data. See Server-side data.

Reference

Grid props from the types
sort SortState[] Default undefined

Controlled sort order (multi-key, primary first). When set, the grid reflects this and reports changes via onSortChange instead of holding its own state. Omit for uncontrolled sorting.

Example
<Grid {rows} {columns} height={480} {sort} onSortChange={(next) => (sort = next)} />
onSortChange (sort: SortState[]) => void

Called with the new sort order whenever a header is clicked.

Example
<Grid {rows} {columns} height={480} onSortChange={(sort) => save('sort', sort)} />
filter string Default ''

Quick-search text: a row is kept when any column's value, as text, contains it (case-insensitive). Bind it to your own search input.

quickFilter boolean Default false

Show a built-in quick-filter search box above the grid that matches across all column values (ANDed with the filter prop). With a source, its text is sent to getRows as quickFilter.

filterRow boolean Default false

Show a per-column filter input row under the header. Rows must match every non-empty column filter (AND). In-memory mode only.

filterMenu boolean Default false

Enable a per-column header filter menu (lazy-loaded on first open). Each filterable column shows a funnel; the menu's control matches the column type (text/number/date). Override or disable per column with col.filter. Works in source mode too (filters are delegated to the RowSource); set filters need in-memory data.

columnFilters Record<string, AnyFilter> Default undefined

Controlled column filters (keyed by column key). When set, the grid reflects these and reports changes via onFilterChange instead of holding its own. Omit for uncontrolled filtering.

Example
<Grid {rows} {columns} height={480} filterMenu
  columnFilters={{ sym: { kind: 'text', op: 'starts', q: 'V' }, last: { kind: 'number', op: 'gt', a: 50 } }} />
onFilterChange (filters: Record<string, AnyFilter>) => void

Called with the full column-filter map whenever a header filter changes.

Example
<Grid {rows} {columns} height={480} filterMenu {columnFilters} onFilterChange={(next) => (columnFilters = next)} />
liveSort boolean | number Default false

Keep the sort and filters current while api.patchRows changes values in place. When a patch changes a field the grid sorts or filters by, the rows it changed are merged back into place at most every liveSort ms (true: 500), instead of the whole view being re-sorted. The selection stays on the same rows; nothing moves while a cell is being edited. Off, rows hold their place until api.refresh(). It follows patchRows; values written into rows any other way still need api.refresh().

Example
<Grid {rows} {columns} height={480} liveSort={250} />
Column options from the types
sortable boolean Default true

Set false to disable header-click sorting on this column.

compare (a: unknown, b: unknown) => number

Custom ascending comparator for this column's values (e.g. enum priority or natural sort). Direction is applied by the grid. In-memory mode only.

Example
{ type: 'badge', key: 'status', header: 'Status', compare: (a, b) => RANK[String(a)] - RANK[String(b)] }
filter false | FilterKind | (string & {}) Default 'date' for date columns, 'number' for number types, else 'text'

Header filter-menu control for this column (requires filterMenu on <Grid>). 'set' shows a value checklist, a registered filterTypes name draws that filter, and false disables filtering for this column.

Example
{ type: 'badge', key: 'status', header: 'Status', filter: 'set' }
Grid API from the types
refresh (): void

Re-run filter and sort against the rows' current values. Values that change in place (a feed) never re-sort the view by themselves — that would mean re-sorting on every tick — so call this when the order should catch up, e.g. every second on a "top movers" board (or use liveSort).

Example
const timer = setInterval(() => api.refresh(), 1000);