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" },
{ key: "orderDate", label: "Ordered", type: ColumnType.DATE, filter: "tree" }, // year › month › day
];
<Grid
rowData={rows}
columnDefs={columns}
quickFilter={{
mode: "always",
behavior: "filter",
matchMode: "multiTerm",
debounceMs: 0,
showOptions: true,
showBehaviorToggle: true,
showLayoutOptions: true,
}}
onFilterChanged={(ev) => recomputeSummary(ev.source, ev.changedColIds)}
onQuickFilterFindChanged={(ev) => setMatches(ev.activeIndex, ev.matchCount)}
/>
columns: NgColDef[] = [
{ key: "customer", label: "Customer", filter: "text" },
{ key: "status", label: "Status", filter: "set" },
{ key: "revenue", label: "Revenue", filter: "number" },
{ key: "orderDate", label: "Ordered", type: ColumnType.DATE, filter: "tree" }, // year › month › day
];
<awb-grid
[rowData]="rows"
[columnDefs]="columns"
[quickFilter]="{ mode: 'always', behavior: 'filter', matchMode: 'multiTerm', debounceMs: 0, showOptions: true, showBehaviorToggle: true, showLayoutOptions: true }"
(filterChanged)="recomputeSummary($event)"
(quickFilterFindChanged)="onFind($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" },
{ key: "orderDate", label: "Ordered", type: ColumnType.DATE, filter: "tree" }, // year › month › day
],
quickFilter: {
mode: "always",
behavior: "filter",
matchMode: "multiTerm",
debounceMs: 0,
showOptions: true,
showBehaviorToggle: true,
showLayoutOptions: true,
},
});
api.setQuickFilter("EMEA on track");
// One canonical signal for column filters AND quick filter:
api.on("filterChanged", (ev) => recomputeSummary(ev.source, ev.changedColIds));
// Or search without filtering: every row stays, matching cells are highlighted.
api.setQuickFilter("EMEA", { behavior: "find" });
api.findNext(); // steps and reveals the match; the box moves aside if it covers it
api.on("quickFilterFindChanged", (ev) => setMatches(ev.activeIndex, ev.matchCount));
Column filters
Set filter: true to infer the UI from ColumnType, or choose "text",
"number", "date", "boolean", "dropdown", "set", or "tree". A named
kind wins over the column type. 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: AND keeps a row that matches every condition, OR one that
matches any. The join (FilterItem.join, "and" when absent) applies between
one column's conditions; filters on different columns are always ANDed.
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;
Without filterOptions, each kind offers these operators, in this order:
| Kind | Operators |
|---|---|
text (and filter: true on a string column) | Contains, Excludes, Equal, Not equal, Starts with, Ends with, Is blank, Is not blank |
number, date | Equal, Not equal, Greater than, Greater than or equal, Less than, Less than or equal, Between, Not between, Is blank, Is not blank |
boolean, dropdown | Equal, Not equal, Is blank, Is not blank |
set, tree | the value checklist |
Blank means null, undefined, or "". Between and Not between take two
inputs, and Between includes both ends.
Dates compare by calendar day in the browser's time zone. The operands are
the date input's yyyy-mm-dd text, or a Date, epoch number, or ISO text
through the API; cells may hold any of the same. Equal means "on that day",
Greater than "after it", Greater than or equal "on or after it", Less than
"before it", Less than or equal "on or before it", and Between runs from the
start of the first day through the end of the second. A cell that is not a date
fails every comparison except Not equal and Not between. This applies to
ColumnType.DATE columns and to any column with filter: "date"; on the
server-side row model the request carries the operands exactly as entered.
Booleans get an Any / True / False choice. true, 1, and the text true,
yes, y, or 1 (any case) count as true; false, 0, false, no, n,
and 0 as false; anything else is neither, so it fails Equal and passes Not
equal.
Dropdown (filter: "dropdown") picks one value from a static
filterParams.filterValues list (["Open", "Closed"], or { value }
entries) and matches it with Equal or Not equal. Without a static list it
falls back to a text box; use filter: "set" for values read from the rows.
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: ["Open", "Pending", "Closed"],
},
} satisfies ColDef;
const asyncSet = {
key: "owner",
label: "Owner",
filter: "set",
filterParams: {
filterValues: async ({ success }) => {
success(await (await fetch("/api/owners")).json());
},
},
} satisfies ColDef;
A static list holds the values themselves — strings, numbers, Dates, or the
objects a keyed column holds — in the order they should appear, which a static
or async list keeps in both layouts. An entry written as { value: "Open" }
(the older form) is unwrapped, so existing lists keep working; a column whose
cell objects have their own value field lists them wrapped that way.
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; without one, values are identified the same way on
both sides — text folded unless caseSensitive, a Date by its instant, an
object by its content. valueFormatter controls labels, mini-filter matching,
accessible names, and value ordering. comparator replaces that ordering:
const rank = { Low: 0, Medium: 1, High: 2 };
const prioritySet = {
key: "priority",
label: "Priority",
filter: "set",
filterParams: { comparator: (a, b) => rank[a.label] - rank[b.label] },
} satisfies ColDef;
It sees each side's raw value and the label the menu shows, applies to the
universe read from the rows (a static or async filterValues list keeps the
order it was given, in either layout), and is never handed a blank: (Blanks)
stays pinned above the values. In the tree layout it orders the siblings at
every level and also receives segment, level, and path; a group has no
value. 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, and
a built-in count is the checkbox's accessible description ("3 rows"). 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.
Tree layout
filter: "tree" is the set filter with its values laid out as a collapsible
tree. A date column needs nothing else: its values group by year and month,
with the day as the leaf. Any other column groups along the path
treePathGetter returns for each value, root first and the leaf last:
const openedColumn = {
key: "opened",
label: "Opened",
type: ColumnType.DATE,
filter: "tree",
filterParams: { treeDefaultExpanded: 1 }, // years open, months closed
} satisfies ColDef;
const productColumn = {
key: "product",
label: "Product",
filter: "tree",
filterParams: {
// "Hardware/Laptops/Pro 14" → Hardware › Laptops › Pro 14
treePathGetter: value => value.split("/"),
},
} satisfies ColDef;
Every segment but the last becomes a group; values that share a prefix share
those groups, and a value whose path is null or empty sits at the root with
its ordinary label. treePathFormatter(segment, level, parentPath) names each
segment — the built-in date path names its months in the column's formatter
locale, and an application path shows String(segment) unless it formats.
Siblings sort by their segment when both are numbers or dates, and by label
otherwise, when the values come from the rows; a static or async filterValues
list keeps the order it was given, as in the flat layout, so siblings appear in
the order their values are listed.
Quarters and weeks are a path getter away. The getter receives the raw cell
value — a Date on a column holding dates; parse ISO text first on a text
column — and the leaf can be the date itself, formatted by level:
const openedColumn = {
key: "opened",
label: "Opened",
type: ColumnType.DATE,
filter: "tree",
filterParams: {
// 2024 › Q1 › 15 Jan: the year, the quarter, then the date itself as the leaf
treePathGetter: (d: Date) => [d.getFullYear(), Math.floor(d.getMonth() / 3) + 1, d],
treePathFormatter: (segment, level) =>
level === 1 ? `Q${segment}`
: level === 2 ? segment.toLocaleDateString(undefined, { month: "short", day: "numeric" })
: String(segment),
treeDefaultExpanded: 1, // years open, quarters closed
},
} satisfies ColDef;
Weeks need the ISO week-year rather than the calendar year, since the first days of January can belong to the last week of the year before:
/** ISO 8601 week: the year the week belongs to and its number, weeks starting Monday. */
function isoWeek(d: Date): [year: number, week: number] {
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7)); // the week's Thursday names the year
const week = Math.ceil(((t.getTime() - Date.UTC(t.getUTCFullYear(), 0, 1)) / 86400000 + 1) / 7);
return [t.getUTCFullYear(), week];
}
const filterParams: FilterParams = {
// 2024 › Week 3 › 15 Jan
treePathGetter: (d: Date) => [...isoWeek(d), d],
treePathFormatter: (segment, level) =>
level === 1 ? `Week ${segment}`
: level === 2 ? segment.toLocaleDateString(undefined, { month: "short", day: "numeric" })
: String(segment),
};
Quarters and weeks are numbers and the leaves are dates, so every level sorts in calendar order; the leaf's day and month follow the browser's locale.
Checking a group checks every leaf beneath it, a partly checked group shows as
mixed, and showValueCounts sums the leaves per group. The mini filter keeps a
row when its own label or an ancestor's matches, so typing a year keeps that
year's months and days, and it holds every matching group open for as long as
the text is there. A leaf also matches on its formatted value, the text the
flat layout would have shown: a date column prints 2026-01-12 by default
(formatterOptions.format changes that, or filterParams.valueFormatter for
the filter alone), so typing the date narrows the tree to the day although the
row only reads "12". Because that text is not on screen, the box's placeholder
shows an example taken from the first value, "Type to filter values, e.g.
2026-01-12"; miniFilterPlaceholder replaces the placeholder in either layout.
treeDefaultExpanded mirrors groupDefaultExpanded: 0
(the default) opens nothing, N opens the first N levels, -1 opens all. The
groups a user opens or closes are remembered for as long as the grid lives, so
the filter reopens as it was left (a group not seen before starts at the
default depth); treeRememberExpansion: false reopens at the default depth
every time.
groupComponent replaces a group row's text the way valueComponent does for
a value, while the grid keeps the chevron and the checkbox. It receives the
label, the summed count, the level, the path, the group's own segment,
and expanded, and is refreshed when the group opens or closes:
const productColumn: ReactColDef = {
key: "product",
label: "Product",
filter: "tree",
filterParams: {
treePathGetter: value => value.split("/"),
showValueCounts: true,
groupComponent: ({ label, count }) => <span>{label} <small>{count} items</small></span>,
},
};
Groups are a presentation of the same universe, never values: the filter model
holds leaf values exactly as in the flat layout, so saved filter state, the Set
Filter API below, and a server-side data source are unchanged. Use keyCreator
to merge values that should be one leaf — a date column holding times, keyed by
its calendar day.
The Select All row is the tree's root: its chevron shows whether every group is open, none is, or only some are (a dash, like the indeterminate checkbox beside it), and opens every group when any is closed or closes them all once all are open.
Keyboard: Up/Down walk the visible rows, Right opens a group or steps into it, Left closes it or moves to its parent (Select All, for a row at the top of the tree), Home/End jump to the first and last row, and Space toggles the row's checkbox. On Select All, Right and Left open and close every group.
Assistive technology sees a tree of treeitems. Select All is the root and
every other row sits beneath it, each announced with its level, its position
among the siblings in view, whether it is open, and its checked state (mixed
for a partly checked group); a value count reads as "3 rows". The row is the
checkable item, so the checkbox inside it is not exposed on its own.
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:
api.setFilterModel([
{ colId: "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.
A tree-layout column adds helpers that address its groups by path:
await api.getSetFilterTree("opened"); // [{ path: [2024], label: "2024", checked: "mixed", children: [...] }, ...]
await api.uncheckSetFilterPath("opened", [2024, 1]); // hide January 2024 — every day under it
await api.checkSetFilterPath("opened", [2024]); // show all of 2024 again
A path names a group, or one leaf by its full path. Segments compare by type
and value as the tree built them, so the built-in date path takes numbers.
Each node carries its label, its count when showValueCounts is on, and
checked ("mixed" for a partly checked group); leaves carry their value,
the blanks bucket as a root leaf with value: null. The filter model stays
leaf-only, so these are the same in/notIn defs as the value helpers
produce. Off a tree column they warn and return null or do nothing.
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" });
api.setQuickFilter("42", { matchMode: "wholeCell" });
Three match modes, all compared against each cell's formatted display value
(so the user searches what they see, $1,200.00 included):
matchMode | Matches when | Notes |
|---|---|---|
multiTerm (default) | every whitespace-separated word is found | while filtering, the words may sit in different cells of the row; while finding, all of them must be in the one cell being highlighted |
substring | the whole input is one contiguous run inside a cell | a run never spans two columns |
wholeCell | a cell's entire text equals the input | no partial hits: 42 matches neither 142 nor 4.2. Both sides are trimmed |
wholeCell is the exact lookup a global search otherwise lacks — "the row whose
id is exactly 42", without knowing which column holds it. Its one sharp edge is
formatting: a column rendered $1,200.00 has to be typed that way, since
equality has none of substring's forgiveness. Note also that it cannot express
"cells that are empty" — an empty search box means no search — which stays the
job of a column filter's isBlank. It is not the same comparison as a column
eq filter either: that normalizes the raw value through textFormatter /
trimValues, while the quick filter compares what is rendered.
On the client-side row model the grid applies the quick filter itself. On the
server-side row model the server does: each getRows (and getAggregates)
request carries the search as request.quickFilter — { text, mode, caseSensitive }, absent while the box is empty — next to the structured column
filters, and a change of text, mode or case refetches from the first block, as
a column-filter change does. See
the request contract.
The floating widget
The search box lives inside the grid, in the strip just below the header and
against its right edge by default. mode decides whether it is there all the
time:
quickFilter: {
mode: "onDemand", // Ctrl/Cmd+F opens it | "always" pins it open
position: { anchor: "right", offsetX: 8, offsetTop: 6 },
clearOnClose: false, // keep filtering after dismissal
showOptions: true, // match-mode + match-case popover
showLayoutOptions: true, // ...plus Anchor and "Keep filter when closed"
}
position moves it: anchor picks the edge, offsetX insets it from that edge
(on top of the scrollbar gutter, so it never sits over the thumb) and offsetTop
is the gap below the header. With clearOnClose: false a dismissed search keeps
filtering, and a compact pill takes the widget's place so the filtering is never
silent — clicking it reopens the box. Every option is listed in
Quick filter options.
The alternative is to hand the widget to the
toolbar, which puts it outside the data region
entirely; position, clearOnClose and showLayoutOptions then do not apply,
because the bar owns the layout.
Find instead of filter
behavior: "find" turns the same box into Excel's Find: nothing is filtered
— every row stays where it is and each cell whose own text contains the search
string is highlighted. The widget counts the matches inside the search box
(3/27 — the active match over the total, 0/0 when nothing matches), with
next/previous steppers beside it.
const options = {
quickFilter: {
mode: "always",
behavior: "find", // "filter" (default) | "find"
showBehaviorToggle: true, // let the end user switch; omit to force `behavior`
caseSensitive: false,
},
} satisfies GridOptions;
api.setQuickFilter("smith", { behavior: "find" });
api.findNext(); // → { rowId, colId, colInstanceId } | null
api.findPrevious();
api.getFindState(); // { behavior, available, text, matchCount, activeIndex, activeMatch }
The two behaviors share the search text, matchMode and caseSensitive, so
switching between them re-runs the same search the other way. Editing those
settings while finding is a search edit, not a filter edit: no rows move, the
page holds, and the selection is left alone. Set behavior
alone to force one of them; add showBehaviorToggle to offer the choice in the
widget's options popover, where the end user's pick is sticky for the session
(like match case, it is not written back to grid options).
A match is always scoped to one cell — a highlight has to land on a single
cell — and matchMode picks the shape within that scope, exactly as it does when
filtering. The one value that reads differently is multiTerm: filtering lets
its words land in different cells of a row, finding requires them all in the cell
it highlights, which makes john smith find Smith, John. wholeCell and
substring mean the same thing in both behaviors.
What is searched, and what is not:
- Every visible data-row cell, across all pages and inside collapsed
groups —
matchCountdoes not depend on what is scrolled into view, andfindNext()reveals its match by expanding ancestors and paging to it. - Not group rows: their cells hold a hierarchy label or an aggregate the grid derives, not a column value. The auto-group column, tree column, row-number and checkbox columns are skipped for the same reason. A hidden column is not searched either.
matchCountcounts cells: a term in three columns of one row is three matches, each separately reachable.
Finding needs data-row cells to highlight, so it is unavailable on the
server-side row model and while the pivot layout is displayed (every displayed
row there is a group row). In both cases getFindState().available is false and
the search falls back to filtering rather than going inert — the widget's toggle
disables itself and says why. On the server-side row model that means the text
is sent to the server as request.quickFilter and narrows the rows; the grid
deliberately does not "find" over the blocks it happens to have loaded, since a
count and next/previous over a partial dataset would be wrong. Asking for
behavior: "find" there logs one console.warn.
Keyboard: Enter steps to the next match, Shift+Enter to the previous, both
wrapping at the ends. Focus stays in the search box, so the cell cursor and the
selection are left alone — stepping through matches is a search gesture, not a
navigation one.
Subscribe to quickFilterFindChanged (or the onQuickFilterFindChanged option
/ the wrappers' quickFilterFindChanged output) to drive an app-owned counter;
a finding quick filter deliberately does not fire filterChanged, since no
rows moved.
Theming: --pte-find-match-bg-color tints every match,
--pte-find-match-active-bg-color plus --pte-find-match-active-border-color
mark the active one — or set all three from one color with the
findMatchColor theme parameter.
Keeping matches visible
While filtering, the widget is chrome you dismiss once the search is typed. While finding it is the opposite: a control surface you keep operating — next, previous, the counter — as you read the cells it points at. So a match that lands underneath it would be unreachable, since dismissing the box to look means losing the steppers.
Stepping to a match therefore moves it out from under the widget. The grid scrolls the match clear where it can, and where no scroll can, the widget itself gives way and flips to the opposite edge. Those unscrollable cases are exactly where the default placement sits:
- the first row, already at the top of its scroll range — the content cannot move down;
- the last column, already at the end of the horizontal range;
- a pinned column, which does not scroll at all;
- a row docked in a frozen band, which stays put wherever the body is scrolled.
The flip lasts as long as the search does. It never rewrites position.anchor or
the end user's Anchor pick: closing the search, emptying the box, or choosing an
anchor brings the widget home. It is also held back while the pointer is over the
widget — so clicking next or previous repeatedly cannot move the button out from
under the cursor — and taken as soon as the pointer leaves. api.findNext() and
api.findPrevious() reveal their match the same way, since they are the same
navigation the steppers perform.
A widget hosted in the toolbar sits outside the data region and never needs any of this.
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). On the server-side model it fires when the refetch is requested, not when the rows arrive. A finding quick filter (behavior: "find") changes no rows and so firesquickFilterFindChangedinstead;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 api.getPaginationInfo() already reflect the new filter:
api.on("filterChanged", () => {
const { totalRowCount, pageIndex, totalPageCount } = api.getPaginationInfo();
status.textContent = `${totalRowCount} rows — page ${pageIndex + 1} of ${totalPageCount}`;
});
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.