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

Example: Sort and filter (sorting-filtering.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef, type SortState } from 'bo-grid';
  import { quotes } from './data';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 110 },
    { type: 'text', key: 'sector', header: 'Sector', width: 140, filter: 'set' },
    { type: 'price', key: 'price', header: 'Price' },
    { type: 'percent', key: 'change', header: 'Chg %' },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1 },
  ];
  let rows = $state(quotes());
  // Controlled sort: start sorted by volume, highest first.
  let sort = $state<SortState[]>([{ key: 'volume', dir: 'desc' }]);
</script>

<Grid {rows} {columns} height={300} filterMenu quickFilter {sort} onSortChange={(s) => (sort = s)} />
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## 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`:

```svelte
<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](https://bonguynvan.github.io/bo-grid/docs/realtime/).

## Quick search

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`):

```svelte
<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.

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

## Filter menu

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

| Column | Operators |
| --- | --- |
| Text | contains, does not contain, equals, starts with, ends with |
| Number | `=`, `≠`, `<`, `≤`, `>`, `≥`, between |
| Date | before, 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.

```ts
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](https://bonguynvan.github.io/bo-grid/docs/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:

```svelte
<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](https://bonguynvan.github.io/bo-grid/docs/server-data/).

## Reference

- `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.

  ```svelte
  <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.

  ```svelte
  <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.

  ```svelte
  <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.

  ```svelte
  <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()`.

  ```svelte
  <Grid {rows} {columns} height={480} liveSort={250} />
  ```


- `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.

  ```ts
  { 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.

  ```ts
  { type: 'badge', key: 'status', header: 'Status', filter: 'set' }
  ```


- `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`).

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

---
Page: https://bonguynvan.github.io/bo-grid/docs/sorting-filtering/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
