Sorting & filtering
Custom filters
Register a filter kind of your own, with its own editor in the column's filter menu, when text, number, date and set filters do not fit.
The built-in filter menu covers text, numbers, dates and sets. When a column needs another rule (a band of values, a lot-size check, “within N km”), register a filter kind with filterTypes and point the column at it with filter: 'name'. The menu keeps its frame (title, Clear, Apply, Enter to apply), and your component edits the middle.
Open the funnel on Chg %: it shows a registered move filter that keeps rows by how far they moved.
Define the filter kind
A filter kind is a test, an optional “is it narrowing anything” check, and an editor component:
import type { CustomFilter, FilterTypeDef } from 'bo-grid';
import MoveEditor from './MoveEditor.svelte';
type Move = 'down' | 'flat' | 'up';
export interface MoveFilter extends CustomFilter {
kind: 'move';
moves: Move[];
}
const moveOf = (pct: number): Move => (pct < -0.5 ? 'down' : pct > 0.5 ? 'up' : 'flat');
export const move: FilterTypeDef<MoveFilter> = {
component: MoveEditor,
isActive: (f) => f.moves.length > 0, // optional; by default always active
test: (value, f) => f.moves.includes(moveOf(Number(value))),
}; test(value, filter, row) decides one cell and runs only while the filter is active.
Register it
Pass the kinds to the grid by name, and select one on a column:
<script lang="ts">
import { Grid, type ColumnDef } from 'bo-grid';
import { move } from './move';
const columns: ColumnDef[] = [
{ type: 'percent', key: 'change', header: 'Chg %', filter: 'move' },
// …
];
</script>
<Grid {rows} {columns} height={480} filterMenu filterTypes={{ move }} /> Write the editor
The editor gets the draft as filter (the active filter when the menu opened, or null) and calls onChange(next) as the user edits. Apply commits the draft when it is active, and Clear removes the filter. Pass null for “no filter”.
<script lang="ts">
import type { FilterEditorProps } from 'bo-grid';
import type { MoveFilter } from './move';
let { filter, onChange }: FilterEditorProps<MoveFilter> = $props();
const picked = $derived(new Set(filter?.moves ?? []));
function toggle(id: 'down' | 'flat' | 'up') {
const next = new Set(picked);
if (next.has(id)) next.delete(id);
else next.add(id);
onChange(next.size ? { kind: 'move', moves: [...next] } : null);
}
</script>
<label><input type="checkbox" checked={picked.has('up')} onchange={() => toggle('up')} /> Up more than 0.5%</label>
<!-- … --> The editor also receives column, labels and, when the kind sets needsValues: true, the column’s distinct values as values (as the set filter uses them).
Saving and server sources
A custom filter is a plain object with a kind, so it goes through columnFilters, onFilterChange and getState() unchanged, and reaches a RowSource in params.columnFilters. To apply it in createArraySource, pass the same kinds:
const source = createArraySource(rows, { filterTypes: { move } }); A filter whose kind is not registered is treated as inactive: it filters nothing, rather than emptying the grid.
Reference
filterTypes FilterTypes Default undefinedCustom filter kinds, by name. A column selects one with filter: 'name'; the header filter menu draws its component and the view keeps rows for which test passes. An unregistered kind filters nothing.
Example
<Grid {rows} {columns} height={480} filterMenu
filterTypes={{ band: { component: BandFilter, test: (v, f) => Number(v) >= f.min && Number(v) <= f.max } }} />filterMenu boolean Default falseEnable 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 undefinedControlled 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)} />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' }