Lattice Grid Buy a licence

developer guide

JavaScript Data Grid Filtering

Filtering runs from the header menu and from code through the same condition tree, so what a reader builds by clicking is exactly what your application can set, read back and store. The operators cover text, numbers, dates, blanks and lists of values, combined with and and or to any depth.

Developer guide › Sorting, filtering and find › JavaScript Data Grid Filtering

Filtering

There are two filters and they are separate on purpose. The quick filter is one string matched across every readable column. The filter set is a structured condition tree.

Quick filter

grid.filters.quick('singapore');
grid.filters.quick('');          // clear

A condition tree

grid.filters.set({
  op: 'and',
  conditions: [
    { col: 'region', op: 'in', value: ['EMEA', 'APAC'] },
    { col: 'utilisation', op: 'gt', value: 0.9 },
    { op: 'or', conditions: [
      { col: 'slaBreached', op: 'eq', value: true },
      { col: 'margin', op: 'lt', value: 0 },
    ]},
  ],
});

That structure is a published wire protocol, not an internal detail. It is what grid.state.get() serialises, what a saved view carries, and what you can send to a server to evaluate the same filter against the full dataset. A remote source hands it straight to your backend.

Operators are per family: text has contains, startsWith, matches; numbers and dates have between, gt, lte; multi-value columns have containsAny, containsAll, containsNone; a geometry column has withinBbox, withinPolygon, intersects and withinDistance (with the geometry module). blank and notBlank work everywhere.

Where a filter's value list comes from

A set filter lists a column's values to tick. Where that list comes from depends on the source, and on a large source it matters: the list is built the moment the popup opens.

  • Memory source (rows): the column's dictionary - every value the grid holds, sorted alphabetically, with a blank last. Nothing is fetched.
  • Pushdown source (createPushdownSource): the engine. Opening the popup sends one GROUP BY on the column with a count through the adapter's executeGroupedAggregates() - DuckDB, ClickHouse and Elasticsearch all answer it - and the browser reads no row to build the list. The popup draws at once, says Loading values…, and fills when the answer lands; closing it aborts the request. The values come most frequent first, each with its count, capped at 200: when the column has more, the popup says so (Top 200 of 8,412 values) and typing in its search box asks the engine again with a contains condition on the column, so a value past the first 200 is still one search away. The list covers the whole relation, not only the rows the grid's current filters match - the same set a memory grid's dictionary lists.
  • A pushdown adapter that cannot answer a distinct-values query (no executeGroupedAggregates(), no count, or a column it cannot group by): the popup offers the text condition editor instead and says why, and the console carries filter:values-unsupported naming the column and the reason.
  • A column with a lookup, or a filter.props.values list: that list, on every source. It is the fastest route and never touches the data.

On a capped list, unticking a value filters with notIn rather than with an in over the values that happened to be listed - the 8,212 values not shown would otherwise vanish with the one you meant to remove. After Clear, ticking builds an in. Either way the condition sent to the engine is exactly what you ticked.

Filters your application owns: where

A condition tree can only test what is in a column. Plenty of real filters cannot be written that way - whether this user may see the row, whether you hold an exchange rate for its currency, whether it came back from your last search call. Those go in as named predicates, and they compose with everything above.

A permission filter and a toggle, side by side

grid.filters.where('visibleToMe', row => row.owner === me, { pinned: true });
grid.filters.where('rateKnown', row => rates.has(row.ccy), { deps: ['ccy'] });

grid.filters.where();              // ['visibleToMe', 'rateKnown']
grid.filters.where('rateKnown', null);   // remove just that one
grid.filters.reapply('rateKnown');       // the rate table arrived late

There is deliberately no "a filter is present" flag. That flag is a second piece of state describing the first, and the two drift: the classic symptom is a grid that filters while the UI insists it is not, or insists it is filtering while every row passes. Here, registering a predicate is what puts it in force, and removing it is what takes it out.

Three options shape one. deps names the columns the predicate reads, exactly as value.deps does for a computed column: the verdict is then cached per row and re-run when one of those columns changes on that row, not when an unrelated one does. Leave it off and the predicate is assumed to read the whole row, so it runs every pass and can never be stale. pinned makes a predicate survive filters.clear(), which is what you want for permissions and tenant scoping and not much else. condition gives the predicate a declarative twin that is pushed to the source while the function stays as the residual, so a pushdown engine narrows the fetch instead of your code filtering a page.

Migrating from an external-filter API: its three pieces become two. isExternalFilterPresent() goes away, because registration is presence. doesExternalFilterPass(node) becomes the predicate itself. onFilterChanged() becomes deps where the grid can watch the change for you, and reapply(name?) where it cannot.

What travels in a saved view is the name, not the function. state.get() carries where: string[]; your predicates are your code and the grid will not pretend it can serialise them. Applying a view that names a predicate you have not registered reports the skip instead of quietly showing a wider row set, and never removes a predicate the view did not mention.