Filtering
Column filters provide type-aware text, number, date, boolean, and set-filter interfaces. Quick filter searches across visible columns.
- React
- Angular
- Core TypeScript
const columns: ReactColDef[] = [
{ key: "customer", label: "Customer", filter: "text" },
{ key: "status", label: "Status", filter: "set" },
{ key: "revenue", label: "Revenue", filter: "number" },
];
<Grid
rowData={rows}
columnDefs={columns}
toolbar={{ quickFilter: true }}
quickFilter={{ matchMode: "multiTerm" }}
onFilterChanged={(ev) => recomputeSummary(ev.source, ev.changedColIds)}
/>
columns: NgColDef[] = [
{ key: "customer", label: "Customer", filter: "text" },
{ key: "status", label: "Status", filter: "set" },
{ key: "revenue", label: "Revenue", filter: "number" },
];
<awb-grid
[rowData]="rows"
[columnDefs]="columns"
[toolbar]="{ quickFilter: true }"
(filterChanged)="recomputeSummary($event)"
/>
const core = new GridCore(measurer, {
columnDefs: [
{ key: "customer", label: "Customer", filter: "text" },
{ key: "status", label: "Status", filter: "set" },
{ key: "revenue", label: "Revenue", filter: "number" },
],
quickFilter: { matchMode: "multiTerm" },
});
api.setQuickFilter("EMEA on track");
// One canonical signal for column filters AND quick filter:
api.on("filterChanged", (ev) => recomputeSummary(ev.source, ev.changedColIds));
Column filters
Set filter: true to infer the UI from ColumnType, or choose "text",
"number", "date", "boolean", "set", or "tree". filterParams
controls operators, debounce, buttons, case handling, and the number of AND/OR
conditions.
const columnDefs: ColDef[] = [
{ key: "name", label: "Name", filter: "text" },
{ key: "amount", label: "Amount", type: ColumnType.NUMBER, filter: "number" },
{ key: "due", label: "Due", type: ColumnType.DATE, filter: "date" },
{ key: "active", label: "Active", type: ColumnType.BOOLEAN, filter: "boolean" },
{ key: "region", label: "Region", filter: "set" },
];
Active filters show an indicator in the header and are managed from the column's filter panel.
const column = {
key: "name",
label: "Name",
filter: "text",
filterParams: {
buttons: ["apply", "clear", "cancel"],
closeOnApply: true,
debounceMs: 200,
maxNumConditions: 2,
caseSensitive: false,
trimValues: true,
},
} satisfies ColDef;
When more than one condition is enabled, the filter UI lets the user join them
with AND or OR. trimValues trims only filter operands, not cell values;
closeOnApply needs an Apply button to have any effect.
Restrict which operators a column offers:
const column = {
key: "amount",
label: "Amount",
filter: "number",
filterParams: {
filterOptions: [
{ value: FilterType.GTE, label: "At least" },
{ value: FilterType.LTE, label: "At most" },
{ value: FilterType.IN_RANGE, label: "Between" },
],
},
} satisfies ColDef;
The built-in operators also include contains, starts/ends with, equality, inequality, blank checks, inclusion, and negated forms.
Set-filter values
Set-filter values can come from loaded rows, a static list, or an asynchronous loader:
const staticSet = {
key: "status",
label: "Status",
filter: "set",
filterParams: {
filterValues: [{ value: "Open" }, { value: "Pending" }, { value: "Closed" }],
},
} satisfies ColDef;
const asyncSet = {
key: "owner",
label: "Owner",
filter: "set",
filterParams: {
filterValues: async ({ success }) => {
success(await (await fetch("/api/owners")).json());
},
},
} satisfies ColDef;
Set filters can keep complex raw values while defining their identity and display separately:
const regionSet = {
key: "region",
label: "Region",
filter: "set",
filterParams: {
keyCreator: value => value.code,
valueFormatter: ({ value }) => value.name,
},
} satisfies ColDef;
keyCreator is used for option deduplication, selection, API lookup, and
client-side comparison; valueFormatter controls labels, mini-filter matching,
accessible names, and value ordering. Filter models and Set Filter API methods
continue to use the raw values. Set filterParams.showValueCounts: true to show the number of loaded
leaf rows for each value. Counts cover the complete client-side row model; with
the server-side row model they are necessarily limited to rows currently loaded
in the grid.
Set-filter label content is independently customizable while the grid retains ownership of the checkbox and interaction behavior:
const regionColumn: ReactColDef = {
key: "region",
label: "Region",
filter: "set",
filterParams: {
showValueCounts: true,
valueComponent: RegionValue,
valueComponentParams: { showFlag: true },
selectAllComponent: SelectAllRegions,
blanksComponent: UnassignedRegion,
},
};
Omitting a component uses the built-in text. Once configured, a component that
renders no content leaves that label slot intentionally empty. Mini-filtering
and accessible checkbox names continue to use the underlying text labels. A
regular value component receives the loaded-row count as count; the Blanks
component receives it too when that option exists.
Blanks are the grid's row, and you can widen it. null, undefined, and
"" are blanks — never 0 or false — decided from the raw value and folded
into (Blanks), so keyCreator and the filter's valueFormatter are never
called with one. Write them for real values: value => value.code needs no
guard.
To count something else as blank, return "" from keyCreator. That value joins
(Blanks), skips the label callback too, and is stored as null in the filter
model — which is what makes a valueGetter-derived value expressible, since only
the application knows which of its shapes are empty.
The column's valueFormatter still sees blanks when rendering cells, which is
how a blank cell shows an em dash. On the server-side row model the bucket travels
as null, so the server applies its own blank rule and cannot see keyCreator.
Custom matcher
A function supplied as filter becomes the row matcher while the standard
filter panel still owns its model and inputs:
const column = {
key: "tags",
label: "Tags",
filter: (value, _node, filterValues) => {
const query = String(filterValues[0] ?? "").toLowerCase();
return value.some((tag: string) => tag.toLowerCase().includes(query));
},
} satisfies ColDef;
For reusable normalization and matching, use filterParams.textFormatter and
filterParams.filterFunction:
const column = {
key: "code",
label: "Code",
filter: "text",
filterParams: {
caseSensitive: false,
trimValues: true,
textFormatter: value => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
filterFunction: (type, values, cell, caseSensitive, trimValues) => {
const query = trimValues ? values[0].trim() : values[0];
const [haystack, needle] = caseSensitive
? [cell, query]
: [cell.toLowerCase(), query.toLowerCase()];
return type === FilterType.STARTS_WITH && haystack.startsWith(needle);
},
},
} satisfies ColDef;
The formatter processes both cell and filter values before comparison. A
filterFunction takes precedence over a function assigned to filter, while
filter: false disables filtering completely. These callbacks execute only in
the client-side row model; server-side filtering must implement the equivalent
normalization on the server.
Programmatic filters
getFilterModel() / setFilterModel(filters) / addFilterModel(filter) /
removeFilterModel(colId) read and write all column filters as serializable
{ colId, filters, join } items addressed by the public ColDef.colId. The
dispatch form addresses runtime columns directly:
const amount = api.getColumnModel().getByColId("amount")!;
api.dispatch({
type: "filterModelSet",
filterModel: [{
col: amount,
key: "amount",
filters: [{ type: FilterType.GTE, values: [100] }],
}],
});
Set filter by intent
For set-filter columns, prefer the intent-level helpers over manipulating
in/notIn defs directly — the set filter manages its own storage:
await api.getSetFilterValues("region"); // the value universe (null = blanks)
await api.uncheckSetFilterValue("region", "EMEA"); // hide EMEA rows
await api.checkSetFilterValue("region", "EMEA"); // show them again
await api.getSetFilterState("region"); // { mode, checked, unchecked } or null
await api.setSetFilterValues("region", ["EMEA"], { mode: "include" });
Inputs are resolved against the column's value universe, so a string "5"
finds the numeric 5 your rows hold, and null addresses the "(Blanks)"
bucket. The methods are async because filter values may come from an async
source; with the default from-rows universe they resolve immediately.
The one semantic worth choosing deliberately is mode — what happens to
values that arrive after filtering (new rows, edited cells):
"exclude"(the menu's default behavior): the unchecked values are stored; anything else, including later arrivals, stays visible."include": the checked values are stored; anything else, including later arrivals, is hidden — "show only what I checked".
An explicit mode passed to setSetFilterValues is pinned on the filter:
subsequent menu or helper toggles keep that representation instead of
optimizing storage to the shorter list.
Quick filter
const options = {
quickFilter: {
mode: "always",
matchMode: "multiTerm",
caseSensitive: false,
debounceMs: 100,
showOptions: true,
},
} satisfies GridOptions;
api.setQuickFilter("open europe");
api.setQuickFilter("exact phrase", { matchMode: "substring" });
multiTerm requires every whitespace-separated term to match somewhere in the
row. substring treats the input as one phrase. The toolbar can host quick
filter, or mode: "onDemand" can open its floating UI with Ctrl/Cmd+F.
Quick filtering is client-side. Server-side applications receive structured column filters in each data-source request.
Reacting to filter changes
Subscribe to the canonical filterChanged event (or pass the onFilterChanged
callback / bind the wrapper's filterChanged output) to recompute anything
filter-derived — footer aggregates, counts, external summaries. It fires once
per effective filter change, whatever the path:
source: "filter"— a column-filter model change (menu, API, dispatch), with the affected publicchangedColIds;source: "quickFilter"— a quick-filter change (changedColIdsis empty; client-side row model only, since the quick filter is a no-op on the server-side model);source: "columns"— a columnDefs update dropped an active filter.
On the client-side row model the handler runs after the view is re-derived, so
row counts and getPaginationInfo() already reflect the new filter.
By default a filter change keeps the user's current page, clamping to the last
page when the result shrinks past it; list "filter" / "quickFilter" in
resetPageOn to jump
back to page 1 instead.