Filtering

One filter config per column, shown on every filter surface; quick filter; the query builder.

Every filter surface reads the same per-column config, filterDef, and shows the same editor:

  • Header funnel: appears on hover, stays visible in the accent colour while the column is filtered, and opens the column's filter popover.
  • Header menu: the column menu's Filter section.
  • Floating filter row (floatingFilters): compact editors under the headers. Typed values filter inline; lists, trees and ranges show a summary chip that opens the full editor.
  • Sidebar Filters panel: every filterable column, its filter summarized when collapsed.
  • Query builder (sidebar Query panel): each condition picks a column and edits it with that column's editor.
  • Filter chips (filterChips): one per active filter; clicking a chip opens its popover.
<Grid rows={rows} columns={columns} filterChips floatingFilters />

Choosing a column's filter

Column types bring a filter that fits them: a select column filters by its options (row counts beside each), a multi-select by any / all / none of its values, a checkbox by Yes / No, a date range by overlap, a cascading select by tree. Set filterDef on a column to choose one yourself; without either a column filters as text.

filterDef.typeEditorCondition
textOperator (contains, is, starts with…, is empty) + value; optional second condition (And / Or){ type: 'text', operator, value }
numberOperator (=, ≥, between…) + value(s); stars: 5 shows a star picker{ type: 'number', operator, value, valueTo }
dateOn, before, after, between (with a calendar); in the last / next N days, weeks…; a period (this week…){ type: 'date', operator, dateFrom, dateTo }
selectSearchable checklist with counts, Select all / Clear, “(Blanks)”; listValues adds Any / All / None of{ type: 'select', values, matchMode }
booleanAll · Yes · No{ type: 'boolean', value }
dateRangeOverlaps / within / includes a date{ type: 'dateRange', operator, dateFrom, dateTo }
pathTree checklist; a node covers everything under it{ type: 'path', paths }
customYour editoranything (often a built-in condition)
noneNo filter—
const columns: ColumnDef<Person>[] = [
	{ field: 'team', header: 'Team', filterDef: { type: 'select', options: [{ value: 'compute', label: 'Compute', color: 'blue' }, { value: 'flight', label: 'Flight' }] } },
	// Without options, the choices are the column's distinct values with row counts.
	{ field: 'region', header: 'Region', filterDef: { type: 'select' } },
	{ field: 'tags', header: 'Tags', filterDef: { type: 'select', listValues: true } },
	{ field: 'salary', header: 'Salary', filterDef: { type: 'number', format: { format: 'currency', currency: 'USD' } } },
];

Server-loaded lists wait debounceMs after typing (250 by default, through the grid's scheduler), search only past minQueryLength, and keep one request in flight: a newer search waits for it, then the latest one runs. Typed text and number filters apply after a short pause, and at once on Enter.

Relative dates

Date filters can roll with the calendar: they are stored relative, so a saved view or a persisted filter keeps meaning "the last 30 days" tomorrow. Cells match by local calendar day, today included.

api.setState({
	filter: {
		columns: {
			created: { type: 'date', operator: 'inLast', dateFrom: '', amount: 30, unit: 'day' }, // 'day' | 'week' | 'month' | 'year'
			due: { type: 'date', operator: 'period', dateFrom: '', period: 'thisWeek' }, // today, lastMonth, thisQuarter, lastYear…
		},
	},
});

"This week" starts on Monday; set filterDef: { type: 'date', weekStartsOn: 0 } for Sunday.

The grid's filter is one piece of state, filter, with parts that a row must all pass:

  • columns: each column's own filter (the header funnel, floating filters, the Filters panel);
  • query: the query builder's conditions;
  • lenses: narrowings the person turns on over whatever view is open (the workspace's chips), { id, label, filter };
  • search: one text matched across fields (the workspace search box), { text, fields? }.

setState({ filter }) changes the parts you give and keeps the others (lenses is replaced whole); filter: null clears them all. A server or infinite datasource receives the same filter with each request, lenses included: AND each lens with the rest.

Lenses narrow, never replace. A view's columns and query are its own; a lens is ANDed with them, so a view keeping status = confirmed and a lens status = draft match nothing (getStatus().lenses says the view narrows the same field, and the workspace says why). Lenses stay on as views switch, are kept in the person's record with the search, and are not part of a view: Save keeps them out, and api.views.saveAs(name, { includeLenses: true }) folds them into the new view's filter. A lens naming a field the grid does not have is inactive, with the reason; one on a field a view hides still narrows.

api.setState({ filter: { lenses: [{ id: 'mine', label: 'Mine', filter: { buyer: { type: 'select', values: ['018'] } } }] } });
const [mine] = api.getStatus().lenses; // { id, label, active, reason, alsoInView }
api.setState({ filter: { search: { text: 'nvda', fields: ['symbol', 'name'] } } }); // these fields
api.setState({ filter: { search: { text: 'nvda' } } }); // every field
const { columns, search, query } = api.getState().filter; // null where unset
api.setState({ filter: null }); // clear everything

Reading and setting filter state

api.setState({
	filter: {
		columns: {
			team: { type: 'select', values: ['compute', 'flight'] },
			tags: { type: 'select', values: ['urgent'], matchMode: 'all' },
			salary: { type: 'number', operator: 'gte', value: 100000 },
		},
	},
});
api.setState({ filter: { columns: { salary: null } } }); // remove one; team and tags stay
api.setState({ filter: { columns: null } }); // clear all

columns is a patch by field: the fields you give change, null removes one, and the rest stay. To set the whole map at once, clear it first (filter: { columns: null }) or replace the whole filter (filter: null, then set it). A saved layout or view applied replaces the column filters exactly.

Filters are plain objects, so they persist with the grid state and saved views, and server-side row models receive them as they are.

Query builder

The query model combines conditions across columns in nested AND / OR groups. Each condition holds a column and one of the filter conditions above, matched by the same code as column filters:

api.setState({
	filter: {
		query: {
			version: 1,
			root: {
				kind: 'group',
				id: 'root',
				operator: 'or',
				children: [
					{ kind: 'condition', id: 'a', columnId: 'team', filter: { type: 'select', values: ['compute'] } },
					{ kind: 'condition', id: 'b', columnId: 'salary', filter: { type: 'number', operator: 'gt', value: 200000 } },
				],
			},
		},
	},
});

Following the filters

The filters are state: follow them with watch.

api.watch(
	(grid) => grid.filter.columns,
	(columns) => console.log(columns)
);
api.watch(
	(grid) => grid.filter.search,
	(quickFilter) => console.log(quickFilter?.text)
);
Edit on GitHub

Last updated on

On this page