Filter builder
Composing AND/OR filter expressions across columns with the opt-in filter builder.
@workspace/active-grid 11.0.0Status unknowncontent differs from the recorded release 11.0.0baseline ce081dc3
This guide is the package's docs/, included
unchanged from the package as it is in Foundation. Links to the package's
internal design documents keep their text.
The filter-builder is an advanced UI for composing AND/OR filter expressions
across multiple columns. It lives alongside the regular per-column filter row
and is opt-in via enable.
Mode support — client only#
The filter-builder works in mode="client" only. The composed expression is
evaluated in the browser into a row-predicate; nothing about it is serialised
onto the wire. Server carries filters, sorting and
global — it has no expression field, and the resource layer in
@workspace/ has no counterpart to receive one.
Consequently, on a mode="server" grid the toolbar button and the panel are
not rendered, and setting enable emits a dev-mode warning
(once per grid, silent in production). Withholding the UI is deliberate: a
rendered builder would invite a user to compose an expression that the grid then
discards without saying so. In server mode, use the per-column filter row — those
filters do reach the server.
Wiring the expression tree into the server query is roadmap item F- (an
expression field on Server plus a recursive translation with the
same allowlist and type validation that individual filters already get). Until
that lands, this is a client-mode feature.
The same applies to external, which shares the predicate path: it is
applied in client mode only, and dev-warns in server mode. See
../.
When to use it#
Use the regular per-column filter row when consumers compose filters one column at a time (the default for most data-grids). Use the filter-builder when consumers need:
OR-logic between conditions: "name contains 'smith' OR email ends with '@acme.com'"
Mixed AND/OR composition: "(role = admin AND status = active) OR (createdAt > 2024 AND status = trial)"
Persistent named filters: the filter-tree serializes to a stable shape for URL-sync or saved-views
Enabling#
import {ActiveGrid} from '@workspace/active-grid';
<ActiveGrid gridId="users" mode="client" data={users} columns={columns} enableFilterBuilder/>;The filter-builder panel appears in the toolbar. The regular per-column filter row remains active — the two are combined with AND.
A condition's value uses the grid's own controls. A number is typed in the
notation of the grid's presentation (the same input as the number filter and
the number editor) and stored as a number; half-typed or unreadable text leaves
the condition's value as it was. A date is typed in the presentation's date
notation or picked from a calendar behind its own button, and stored as an ISO
calendar day (YYYY-). Emptying a number or date field makes the value
absent, as in a new condition. Other columns take text.
A condition filters only once it is complete. Until its operator has every
value it reads, it is ignored: adding a condition, or emptying its value, leaves
the rows as they were. A missing value is undefined, null, '' or a
non-finite number; 0 and false are values. A range (between, in)
needs both ends. The no-value operators is and is are complete
without a value. An AND or OR group judges only its complete conditions, and a
group with none matches every row. The toolbar button follows the same rule: its
count and its active state show only the conditions that filter, so an
unfinished condition can stay in the panel without the grid looking filtered.
The panel's own count and its clear action cover every condition in it.
On mode="server" this same configuration renders no button and no panel, and
dev-warns instead. See Mode support.
The expression tree#
A filter expression is a tree of Filter-leaves wrapped in
Filter-nodes with combinator: 'and' | 'or':
import type { FilterExpression, FilterCondition, FilterGroup, FilterExpressionNode,} from '@workspace/active-grid';
// A filter condition on one columninterface FilterCondition { id: string; field: string; // column id operator: FilterOperator; value?: unknown; valueTo?: unknown; // for range operators}
// A group with AND/OR logicinterface FilterGroup { id: string; combinator: 'and' | 'or'; children: FilterExpressionNode[]; // can contain conditions or nested groups}
// FilterExpressionNode is a discriminated uniontype FilterExpressionNode = FilterCondition | FilterGroup;
// The root is always a grouptype FilterExpression = FilterGroup;The is type-guard narrows a Filter to
Filter (it returns true when the node has a combinator field).
Example: composed expression#
const expression: FilterExpression = { id: 'root', combinator: 'or', children: [ { id: 'group-1', combinator: 'and', children: [ {id: 'c1', field: 'role', operator: 'equals', value: 'admin'}, {id: 'c2', field: 'status', operator: 'equals', value: 'active'}, ], }, { id: 'group-2', combinator: 'and', children: [ { id: 'c3', field: 'createdAt', operator: 'greaterThan', value: new Date('2024-01-01'), }, {id: 'c4', field: 'status', operator: 'equals', value: 'trial'}, ], }, ],};This represents (role = admin AND status = active) OR (created.
Initial expression and change observation#
Uncontrolled (recommended)#
Let the grid manage its own expression-state. Optionally seed it with
initial.
<ActiveGrid enableFilterBuilder initialFilterExpression={savedExpression} // .../>Observe changes#
Subscribe to changes via on for a host-owned saved-view
integration. initial seeds the grid; it is not a controlled
value prop. Observing the expression does not transfer its runtime ownership
to the host.
const [expression, setExpression] = useState<FilterExpression | null>(null);
<ActiveGrid enableFilterBuilder initialFilterExpression={savedExpression} onFilterExpressionChange={(expr) => { setExpression(expr); // Persist to URL or saved-view storage }} // .../>;Evaluating expressions client-side#
The grid evaluates the expression internally. If you need to filter rows
yourself (e.g., for a custom row-list outside the grid), use
evaluate:
import {evaluateFilterExpression} from '@workspace/active-grid';
const filtered = rows.filter((row) => evaluateFilterExpression(row, expression),);That evaluator is exported independently of the grid, so a consumer who needs
AND/OR composition against a server dataset can build the expression tree
themselves and translate it in their own query layer. The grid does not do that
translation for them today — that is F-.
Combining with per-column filters#
The per-column filter row stays active when enable is on.
The two are combined with AND. A user can pin a per-column filter (e.g.
"status = active") while building a complex expression over other columns.
Persistence — not built in#
The filter-expression is not persisted by the grid. Neither driver carries
it: enable serialises page, page, sort, filters and
search (src/), and enable validates and
stores sorting, column, column, column and
global (src/). There is no
filter key in either.
To persist an expression, own it in the consumer: subscribe with
on, write it wherever you keep view-state, and seed it
back through initial on mount. Keep the tree
JSON-serializable (no Date-objects — use ISO strings) if you round-trip it
through a URL or storage.
A11y#
The filter-builder panel uses the shared Base UI primitives and provides full keyboard navigation. Tab through the group/condition tree; Enter to open operator-selects; Escape to close popovers.
Reference#
Filter,Expression Filter,Condition Filter,Group Filter,Expression Node is— exported fromFilter Group @workspace/active- grid evaluate— client-side row evaluatorFilter Expression(row, expression) Server mode: the builder is withheld and dev-warns; per-column filters are the server-capable route (see
../)api/ resource- mode. md API reference:
../api/ filters. md