Filtering Plugin
The Filtering plugin adds column header filters with text search, dropdown options, and custom filter panels. It supports both local filtering for small datasets and async handlers for server-side filtering on large datasets.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/filtering';Basic Usage
Section titled “Basic Usage”import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/filtering';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ], features: { filtering: { debounceMs: 300 } },};grid.rows = data;import '@toolbox-web/grid-react/features/filtering';import { DataGrid } from '@toolbox-web/grid-react';
function MyGrid({ data }) { return ( <DataGrid rows={data} columns={[ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ]} filtering={{ debounceMs: 300 }} style={{ height: '400px' }} /> );}<script setup>import '@toolbox-web/grid-vue/features/filtering';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
const data = [ { name: 'Alice', status: 'active', email: 'alice@example.com' }, { name: 'Bob', status: 'inactive', email: 'bob@example.com' },];</script>
<template> <TbwGrid :rows="data" :filtering="{ debounceMs: 300 }" style="height: 400px"> <TbwGridColumn field="name" header="Name" filterable /> <TbwGridColumn field="status" header="Status" filterable /> <TbwGridColumn field="email" header="Email" filterable /> </TbwGrid></template>// Feature import - enables the [filtering] inputimport '@toolbox-web/grid-angular/features/filtering';
import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [columns]="columns" [filtering]="{ debounceMs: 300 }" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { rows = [...];
columns: ColumnConfig[] = [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, { field: 'email', header: 'Email', filterable: true }, ];}Default Filtering
Section titled “Default Filtering”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/filtering';
const sampleData = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, status: 'Active' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, status: 'Active' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, status: 'On Leave' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, status: 'Active' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, status: 'Inactive' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, status: 'Active' }, { id: 7, name: 'Grace Lee', department: 'Sales', salary: 82000, status: 'Active' }, { id: 8, name: 'Henry Wilson', department: 'HR', salary: 68000, status: 'Active' }, { id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 112000, status: 'Active' }, { id: 10, name: 'Jack Taylor', department: 'Marketing', salary: 78000, status: 'On Leave' },];const columns = [ { field: 'id', header: 'ID', type: 'number', filterable: false }, { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number' }, { field: 'status', header: 'Status' },];
const grid = queryGrid('tbw-grid')!;
function rebuild(debounceMs = 150, caseSensitive = false) { grid.gridConfig = { columns, features: { filtering: { debounceMs, caseSensitive } }, }; grid.rows = sampleData;}
rebuild();Type in column header filter inputs to filter rows. The plugin debounces input to avoid excessive re-filtering while typing.
Custom Filter Panel
Section titled “Custom Filter Panel”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/filtering';
// Sample data for filtering demosconst sampleData = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, status: 'Active' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, status: 'Active' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, status: 'On Leave' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, status: 'Active' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, status: 'Inactive' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, status: 'Active' }, { id: 7, name: 'Grace Lee', department: 'Sales', salary: 82000, status: 'Active' }, { id: 8, name: 'Henry Wilson', department: 'HR', salary: 68000, status: 'Active' }, { id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 112000, status: 'Active' }, { id: 10, name: 'Jack Taylor', department: 'Marketing', salary: 78000, status: 'On Leave' },];const columns = [ { field: 'id', header: 'ID', type: 'number', filterable: false }, { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number' }, { field: 'status', header: 'Status' },];
const grid = queryGrid('tbw-grid');
// Custom filter panel for Status column (radio-button style) const statusFilterPanel = (container, params) => { container.innerHTML = ` <div style="padding: 8px; min-width: 140px;"> <div style="font-weight: 600; margin-bottom: 8px; color: var(--tbw-color-fg);"> Filter by Status </div> <div class="status-options"></div> <button class="clear-btn" style="margin-top: 8px; width: 100%; padding: 6px; cursor: pointer;"> Clear </button> </div> `;
const optionsDiv = document.querySelector('.status-options'); if (!optionsDiv) return;
const options = ['All', ...params.uniqueValues.map((v) => String(v))];
options.forEach((opt) => { const label = document.createElement('label'); label.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer;'; const isAll = opt === 'All'; label.innerHTML = `<input type="radio" name="status" value="${opt}" ${ isAll && params.excludedValues.size === 0 ? 'checked' : '' }> ${opt}`; const input = label.querySelector('input'); if (input) { input.addEventListener('change', () => { if (isAll) { params.clearFilter(); } else { // Exclude all except selected const excluded = params.uniqueValues.filter((v) => String(v) !== opt); params.applySetFilter(excluded); } }); } optionsDiv.appendChild(label); });
const clearBtn = document.querySelector('.clear-btn'); if (clearBtn) { clearBtn.addEventListener('click', () => params.clearFilter()); } };
grid.gridConfig = { columns, features: { filtering: { debounceMs: 150, caseSensitive: false, filterPanelRenderer: (container, params) => { if (params.field === 'status') { statusFilterPanel(container, params); } else { return undefined; // Use default panel } }, }, }, }; grid.rows = sampleData;The filterPanelRenderer option lets you replace the default filter panel with custom UI.
When the renderer returns undefined, the default panel is used for that column.
FilterPanelParams
Section titled “FilterPanelParams”Your custom renderer receives a params object with:
Properties
| Property | Type | Description |
|---|---|---|
field | string | The field being filtered |
column | ColumnConfig | Column configuration |
uniqueValues | unknown[] | All unique values for this field |
excludedValues | Set<unknown> | Currently excluded values (set filter) |
searchText | string | Current search text |
currentFilter | FilterModel? | Active filter model for this field (if any) — use to pre-populate custom UI |
Methods
| Method | Signature | Description |
|---|---|---|
applySetFilter | (excluded: unknown[], valueTo?: unknown) => void | Apply a set filter; optional valueTo stores metadata alongside the filter |
applyTextFilter | (op: FilterOperator, val: string | number, valueTo?: string | number) => void | Apply a text/number/date filter with operator |
clearFilter | () => void | Clear the filter for this field |
closePanel | () => void | Close the filter panel |
Example: Radio-button Filter
Section titled “Example: Radio-button Filter”import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');
grid.gridConfig = { features: { filtering: { filterPanelRenderer: (container, params) => { // Custom panel only for 'status' column if (params.field !== 'status') return undefined;
const activeValue = params.currentFilter?.value;
container.innerHTML = ` <div style="padding: 8px;"> <label><input type="radio" name="status" value="all" ${!activeValue ? 'checked' : ''}> All</label> ${params.uniqueValues .map((v) => `<label><input type="radio" name="status" value="${v}" ${v === activeValue ? 'checked' : ''}> ${v}</label>`) .join('')} </div> `;
container.querySelectorAll('input').forEach((input) => { input.addEventListener('change', () => { if (input.value === 'all') { params.clearFilter(); } else { const excluded = params.uniqueValues.filter((v) => v !== input.value); params.applySetFilter(excluded); } params.closePanel(); }); }); }, }, },};import '@toolbox-web/grid-react/features/filtering';import { DataGrid } from '@toolbox-web/grid-react';
// React filterPanelRenderer receives only params (no container)// and returns JSX — the adapter bridges it automaticallyfunction StatusFilterPanel({ params }) { if (params.field !== 'status') return undefined;
const activeValue = params.currentFilter?.value;
return ( <div style={{ padding: 8 }}> {['all', ...params.uniqueValues].map((v) => ( <label key={v}> <input type="radio" name="status" value={v} defaultChecked={v === 'all' ? !activeValue : v === activeValue} onChange={() => { if (v === 'all') { params.clearFilter(); } else { const excluded = params.uniqueValues.filter((u) => u !== v); params.applySetFilter(excluded); } params.closePanel(); }} /> {v === 'all' ? ' All' : ` ${v}`} </label> ))} </div> );}
function MyGrid({ data }) { return ( <DataGrid rows={data} columns={[ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, ]} filtering={{ filterPanelRenderer: (params) => <StatusFilterPanel params={params} />, }} /> );}<script setup>import '@toolbox-web/grid-vue/features/filtering';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';import { h } from 'vue';
const data = [...];
// Vue filterPanelRenderer receives only params and returns a VNodeconst filterPanelRenderer = (params) => { if (params.field !== 'status') return undefined;
const activeValue = params.currentFilter?.value;
return h('div', { style: 'padding: 8px' }, ['all', ...params.uniqueValues].map((v) => h('label', [ h('input', { type: 'radio', name: 'status', value: v, checked: v === 'all' ? !activeValue : v === activeValue, onChange: () => { if (v === 'all') { params.clearFilter(); } else { const excluded = params.uniqueValues.filter((u) => u !== v); params.applySetFilter(excluded); } params.closePanel(); }, }), v === 'all' ? ' All' : ` ${v}`, ]) ) );};</script>
<template> <TbwGrid :rows="data" :filtering="{ filterPanelRenderer }" style="height: 400px"> <TbwGridColumn field="name" header="Name" filterable /> <TbwGridColumn field="status" header="Status" filterable /> </TbwGrid></template>// Angular uses a component class extending BaseFilterPanelimport { Component, ViewChild, ElementRef } from '@angular/core';import { BaseFilterPanel } from '@toolbox-web/grid-angular';
@Component({ selector: 'app-status-filter', template: ` <div style="padding: 8px;"> @for (value of ['all'].concat(params().uniqueValues); track value) { <label> <input type="radio" name="status" [value]="value" [checked]="value === 'all' ? !activeValue : value === activeValue" (change)="onSelect(value)" /> {{ value === 'all' ? 'All' : value }} </label> } </div> `,})export class StatusFilterComponent extends BaseFilterPanel { get activeValue() { return this.params().currentFilter?.value; }
applyFilter(): void { /* Not used — onSelect handles it */ }
onSelect(value: string) { if (value === 'all') { this.params().clearFilter(); } else { const excluded = this.params().uniqueValues.filter((v) => v !== value); this.params().applySetFilter(excluded); } this.params().closePanel(); }}// In your grid component — pass the component class as filterPanelRendererimport '@toolbox-web/grid-angular/features/filtering';import { Grid } from '@toolbox-web/grid-angular';import { StatusFilterComponent } from './status-filter.component';
@Component({ imports: [Grid], template: `<tbw-grid [rows]="data" [columns]="columns" [filtering]="filterConfig" />`})export class MyGridComponent { data = [...]; columns = [ { field: 'name', header: 'Name', filterable: true }, { field: 'status', header: 'Status', filterable: true }, ];
filterConfig = { filterPanelRenderer: StatusFilterComponent, };}Type-Specific Filter Panels
Section titled “Type-Specific Filter Panels”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/filtering';
const typedData = [ { id: 1, name: 'Alice Johnson', salary: 95000, hireDate: '2020-03-15', rating: 4.5 }, { id: 2, name: 'Bob Smith', salary: 75000, hireDate: '2019-07-22', rating: 3.8 }, { id: 3, name: 'Carol Williams', salary: 105000, hireDate: '2018-11-10', rating: 4.9 }, { id: 4, name: 'Dan Brown', salary: 85000, hireDate: '2021-01-05', rating: 4.2 }, { id: 5, name: 'Eve Davis', salary: 72000, hireDate: '2022-06-18', rating: 3.5 }, { id: 6, name: 'Frank Miller', salary: 98000, hireDate: '2017-09-30', rating: 4.7 }, { id: 7, name: 'Grace Lee', salary: 82000, hireDate: '2020-12-01', rating: 4.0 }, { id: 8, name: 'Henry Wilson', salary: 68000, hireDate: '2023-02-14', rating: 3.2 },];
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', filterable: false }, { field: 'name', header: 'Name' }, // Set filter (default) { field: 'salary', header: 'Salary', type: 'number', // Range slider filter filterParams: { min: 50000, max: 150000, step: 5000 }, }, { field: 'hireDate', header: 'Hire Date', type: 'date', // Date range picker filter filterParams: { min: '2015-01-01', max: '2025-12-31' }, }, { field: 'rating', header: 'Rating', type: 'number', // Range slider with smaller step filterParams: { min: 1, max: 5, step: 0.1 }, }, ], features: { filtering: true }, }; grid.rows = typedData;The Filtering plugin automatically displays appropriate filter UIs based on column type:
| Column Type | Filter UI | Description |
|---|---|---|
number | Range Slider | Dual-thumb slider with min/max inputs. Includes a “Blank” checkbox to filter rows with no value. |
date | Date Range Picker | From/to date inputs with range selection. Includes a “Show only blank” checkbox for empty dates. |
| (other) | Checkbox Set | Standard multi-select with search. Rows with null/undefined/empty values appear as a (Blank) entry. |
Configure bounds and step via filterParams on the column:
const columns = [ { field: 'salary', header: 'Salary', type: 'number', filterParams: { min: 50000, max: 150000, step: 5000 }, }, { field: 'hireDate', header: 'Hire Date', type: 'date', filterParams: { min: '2015-01-01', max: '2025-12-31' }, },];If filterParams is not provided, the plugin auto-detects min/max from the data.
Async Filtering (Server-Side)
Section titled “Async Filtering (Server-Side)”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/filtering';
// Sample data for filtering demosconst sampleData = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, status: 'Active' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, status: 'Active' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, status: 'On Leave' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, status: 'Active' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, status: 'Inactive' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, status: 'Active' }, { id: 7, name: 'Grace Lee', department: 'Sales', salary: 82000, status: 'Active' }, { id: 8, name: 'Henry Wilson', department: 'HR', salary: 68000, status: 'Active' }, { id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 112000, status: 'Active' }, { id: 10, name: 'Jack Taylor', department: 'Marketing', salary: 78000, status: 'On Leave' },];const columns = [ { field: 'id', header: 'ID', type: 'number', filterable: false }, { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number' }, { field: 'status', header: 'Status' },];
function generateMockData(count: number) { const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']; const statuses = ['Active', 'Inactive', 'On Leave']; const data = []; for (let i = 0; i < count; i++) { data.push({ id: i + 1, name: `Employee ${i + 1}`, department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), status: statuses[i % statuses.length], }); } return data;}
const grid = queryGrid('tbw-grid');
const serverData = generateMockData(1000);
// Simulate server-side unique value extraction const getUniqueValues = async (field: string): Promise<unknown[]> => { await new Promise((r) => setTimeout(r, 200)); const values = [...new Set(serverData.map((row) => (row)[field]))]; return values.sort(); };
// Simulate server-side filtering // FilterModel uses { field, operator: 'notIn', value: excludedValues[] } const applyServerFilters = async ( filters: { field: string; operator: string; value: unknown[] }[], ): Promise<typeof serverData> => { await new Promise((r) => setTimeout(r, 300));
if (filters.length === 0) return serverData;
return serverData.filter((row) => { return filters.every((filter) => { const excludedValues = filter.value; if (!excludedValues || excludedValues.length === 0) return true;
const val = (row)[filter.field]; // 'notIn' means exclude these values, so row passes if value is NOT in excluded list return !excludedValues.includes(val); }); }); };
grid.gridConfig = { columns, features: { filtering: { valuesHandler: getUniqueValues, filterHandler: applyServerFilters, }, }, };
grid.rows = serverData;For large or server-side datasets, the default local filtering may not be suitable:
- Not all unique values are available locally for the filter panel
- Filtering should happen on the backend rather than in the browser
Use valuesHandler and filterHandler to customize this behavior:
grid.gridConfig = { columns: [...], features: { filtering: { // Fetch unique values for a column from the server valuesHandler: async (field, column) => { const response = await fetch(`/api/distinct-values?field=${field}`); return response.json(); // Returns: ['Engineering', 'Marketing', ...] },
// Apply filters on the server // FilterModel: { field, type, operator: 'notIn', value: excludedValues[] } filterHandler: async (filters, currentRows) => { const response = await fetch('/api/data', { method: 'POST', body: JSON.stringify({ filters }), }); return response.json(); }, }, },};Handler signatures:
| Handler | Signature | Returns |
|---|---|---|
valuesHandler | (field: string, column: ColumnConfig) => Promise<T[]> | Unique values for filter |
filterHandler | (filters: FilterModel[], rows: T[]) => T[] | Promise<T[]> | Filtered rows |
When valuesHandler is provided, the filter panel shows a loading indicator while fetching values.
When filterHandler is provided, filter application bypasses the local processRows hook.
Configuration Options
Section titled “Configuration Options”Grid-Level Toggle
Section titled “Grid-Level Toggle”You can disable filtering grid-wide using gridConfig.filterable:
grid.gridConfig = { filterable: false, // Disables ALL filtering, even on filterable columns features: { filtering: true },};This is useful for toggling filtering on/off at runtime without removing the plugin.
Plugin Options
Section titled “Plugin Options”| Option | Type | Default | Description |
|---|---|---|---|
debounceMs | number | 300 | Debounce delay for filter input |
caseSensitive | boolean | false | Case-sensitive string matching |
trimInput | boolean | true | Trim whitespace from filter input |
useWorker | boolean | true | Use Web Worker for datasets >1000 rows |
trackColumnState | boolean | false | Include filter state in column state persistence |
filterPanelRenderer | FilterPanelRenderer | - | Custom filter panel renderer |
valuesHandler | FilterValuesHandler | - | Async handler to fetch unique filter values |
filterHandler | FilterHandler<TRow> | - | Async handler to apply filters remotely |
Column Configuration
Section titled “Column Configuration”const columns = [ { field: 'name', filterable: true, // Enable filtering filterType: 'text', // 'text' | 'select' | 'number' | 'date' }, { field: 'status', filterable: true, filterType: 'select', filterOptions: ['Active', 'Inactive', 'Pending'], },];Programmatic API
Section titled “Programmatic API”const plugin = grid.getPluginByName('filtering');
// Set filter valueplugin.setFilter('name', 'Alice');
// Get current filtersconst filters = plugin.getFilters();
// Clear all filtersplugin.clearFilters();
// Clear specific filterplugin.clearFilter('name');
// Silent mode: update filter state without triggering re-render// Useful for batching multiple filter changesplugin.setFilter('name', 'Alice', { silent: true });plugin.setFilter('dept', 'Engineering'); // last call applies allColumn State Persistence
Section titled “Column State Persistence”By default, filter state is not included in column state and does not fire column-state-change.
Enable trackColumnState to opt in:
features: { filtering: { trackColumnState: true } }When enabled:
- Filter state is included in
getColumnState()/columnStatesnapshots - Filter changes fire the
column-state-changeevent (debounced) applyColumnState()/columnStaterestores filter state
This is useful when you want to persist and restore the full grid state (including active filters)
via localStorage or a server:
// Save full state including filtersgrid.on('column-state-change', (state) => { localStorage.setItem('grid-state', JSON.stringify(state));});
// Restore state (filters are re-applied automatically)gridConfig: { columnState: JSON.parse(localStorage.getItem('grid-state')), features: { filtering: { trackColumnState: true } },}Events
Section titled “Events”import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/filtering';
const sampleData = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, status: 'Active' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, status: 'Active' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, status: 'On Leave' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, status: 'Active' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, status: 'Inactive' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, status: 'Active' }, { id: 7, name: 'Grace Lee', department: 'Sales', salary: 82000, status: 'Active' }, { id: 8, name: 'Henry Wilson', department: 'HR', salary: 68000, status: 'Active' }, { id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 112000, status: 'Active' }, { id: 10, name: 'Jack Taylor', department: 'Marketing', salary: 78000, status: 'On Leave' },];
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'status', header: 'Status' }, ], features: { filtering: { debounceMs: 300 } },};
grid.rows = sampleData;
const log = document.querySelector('#filter-event-log');const clearBtn = document.querySelector('#clear-filter-log');
function addLog(type: string, detail: string) { if (!log) return; const msg = document.createElement('div'); msg.innerHTML = `<span class="event-type">[${type}]</span> ${detail}`; log.insertBefore(msg, log.firstChild); while (log.children.length > 15) { log.lastChild?.remove(); }}
clearBtn?.addEventListener('click', () => { if (log) log.innerHTML = '';});
grid.on('filter-change', (d) => { const filterCount = d.filters?.length || 0; const fields = d.filters?.map((f: { field: string }) => f.field).join(', ') || 'none'; addLog('filter-change', `${filterCount} filter(s) on: ${fields}`);});<tbw-grid style="height: 300px;"></tbw-grid> <div class="log-panel"> <div class="log-header"> <strong>Event Log:</strong> <button>Clear</button> </div> <div class="event-log"></div> </div></div>
<style> #filtering-filter-events-demo .log-panel { border: 1px solid var(--sl-color-gray-5); padding: 12px; border-radius: 4px; background: var(--sl-color-gray-6); overflow-y: auto; max-height: 300px; margin-top: 8px; } #filtering-filter-events-demo .log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } #filtering-filter-events-demo .log-header button { padding: 4px 8px; cursor: pointer; font-size: 12px; } #filtering-filter-events-demo .event-log { font-family: monospace; font-size: 11px; color: var(--sl-color-gray-2); } #filtering-filter-events-demo .event-log > div { padding: 2px 0; border-bottom: 1px solid var(--sl-color-gray-5); } #filtering-filter-events-demo .event-log .event-type { color: var(--sl-color-accent); }</style>The FilteringPlugin emits events when filter state changes. Open filter panels and apply filters to see the events:
| Event | Description |
|---|---|
filter-change | Fired when filters are applied, changed, or cleared |
filter-change Detail
Section titled “filter-change Detail”interface FilterChangeDetail { filters: FilterModel[]; // Active filter configurations}Styling
Section titled “Styling”The filter panel and inputs support CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-filter-panel-bg | var(--tbw-color-panel-bg) | Panel background |
--tbw-filter-panel-fg | var(--tbw-color-fg) | Panel text color |
--tbw-filter-panel-border | var(--tbw-color-border) | Panel border |
--tbw-filter-panel-radius | var(--tbw-border-radius) | Panel border radius |
--tbw-filter-panel-shadow | var(--tbw-color-shadow) | Panel shadow |
--tbw-filter-input-bg | var(--tbw-color-bg) | Input background |
--tbw-filter-input-border | var(--tbw-color-border) | Input border |
--tbw-filter-input-focus | var(--tbw-color-accent) | Input focus border |
--tbw-filter-active-color | var(--tbw-color-accent) | Active filter indicator |
--tbw-filter-btn-padding | var(--tbw-button-padding) | Filter button padding |
--tbw-filter-btn-font-weight | 500 | Filter button font weight |
--tbw-filter-btn-min-height | auto | Filter button min height |
--tbw-filter-search-padding | var(--tbw-spacing-sm) var(--tbw-spacing-md) | Search input padding |
--tbw-filter-item-height | 28px | Filter value item height (for virtualization) |
--tbw-filter-btn-display | inline-flex | Filter button display (set to none to hide) |
--tbw-filter-btn-visibility | visible | Filter button visibility |
--tbw-panel-padding | 0.75em | Panel padding |
--tbw-panel-gap | 0.5em | Gap between elements |
--tbw-animation-duration | 200ms | Panel open/close animation |
Theming Filter Panels
Section titled “Theming Filter Panels”The filter panel is rendered in document.body for proper z-index stacking. To apply theme CSS variables:
- Add a theme class to the grid (e.g.,
tbw-grid.eds-theme) - The class is automatically copied to the filter panel
/* Define theme on a CSS class */.eds-theme { --tbw-filter-panel-bg: #f5f5f5; --tbw-filter-btn-padding: 8px 16px; --tbw-filter-btn-font-weight: 500; --tbw-filter-btn-min-height: 48px; --tbw-filter-item-height: 48px;}<!-- Apply class to grid - it cascades to filter panel --><tbw-grid class="eds-theme" ...></tbw-grid>Hiding Filter Buttons Until Hover
Section titled “Hiding Filter Buttons Until Hover”To show filter buttons only when hovering over a column header:
tbw-grid { --tbw-filter-btn-visibility: hidden;}
tbw-grid .header-row .cell:hover .tbw-filter-btn,tbw-grid .header-row .cell .tbw-filter-btn.active { visibility: visible;}Example
Section titled “Example”tbw-grid { /* Custom filter panel styling */ --tbw-filter-panel-bg: #1e1e1e; --tbw-filter-panel-fg: #ffffff; --tbw-filter-panel-border: #444444; --tbw-filter-input-bg: #2d2d2d; --tbw-filter-input-border: #555555; --tbw-filter-active-color: #4fc3f7;}CSS Classes
Section titled “CSS Classes”The filter panel uses these class names for advanced customization:
| Class | Element |
|---|---|
.tbw-filter-panel | Dropdown panel container |
.tbw-filter-search | Text search input |
.tbw-filter-list | Options list container |
.tbw-filter-option | Individual filter option |
.tbw-filter-option.selected | Selected option |
.tbw-filter-buttons | Action button group |
.tbw-filter-clear | Clear filter button |
.tbw-filter-apply | Apply filter button |
Row Insertion with Active Filters
Section titled “Row Insertion with Active Filters”When filtering is active, assigning rows automatically re-filters the data — so a
newly inserted row may be hidden if it doesn’t match the current filter. If you want
the row to appear exactly where you placed it (e.g., after a user clicks “Add Row”),
use insertRow():
grid.insertRow(3, newRow); // stays at visible index 3, auto-animatesThe row is also added to source data, so the next full grid.rows = freshData
assignment re-filters normally. See the
API Reference → Insert & Remove Rows
for full details.
See Also
Section titled “See Also”- Multi-Sort — Multi-column sorting
- Server-Side Data — Server-side filtering via
filterHandler - Selection — Row and cell selection
- Common Patterns — Full application recipes using filtering
- Plugins Overview — Plugin compatibility and combinations