# Data Table Side Filters URL: /docs/web/components/data-table-side-filters An always-visible side filter panel for e-commerce-style listings — covers every Docyrus field type and emits the same RuleGroupType JSON as the Query Builder. **Demo:** ```tsx 'use client'; // @custom-demo import { useMemo } from 'react'; import { DataTableSideFilters, useDataTableSideFilters } from '@docyrus/ui/components/data-table-side-filters'; import { getBrandLabel, getCategoryMeta, getVendorLabel, useSideFilterDemoData } from '@/data/data-table-side-filters-data'; interface Product { id: string; name: string; category: string; brand: string; tags: Array; vendorId: string; price: number; rating: number; inStock: boolean; freeShipping: boolean; releasedAt: Date; } export function DataTableSideFiltersDemo() { const { data, columnsConfig, query, setQuery } = useSideFilterDemoData(); const { columns, filters, actions, strategy } = useDataTableSideFilters({ strategy: 'client', data, columnsConfig: columnsConfig as never, query, onQueryChange: setQuery }); const filtered = useMemo(() => { if (filters.length === 0) return data as Array; return (data as Array).filter((row) => { return filters.every((filter) => { const cell = (row as unknown as Record)[filter.columnId]; return matchesFilter(cell, filter); }); }); }, [data, filters]); return (

{filtered.length} {' of '} {data.length} {' products'}

{filtered.slice(0, 12).map(product => ( ))} {filtered.length === 0 && (
No products match the selected filters.
)}
Emitted query (RuleGroupType — same JSON the QueryBuilder consumes)
            {JSON.stringify(query, jsonReplacer, 2)}
          
); } function ProductCard({ product }: { product: Product }) { const meta = getCategoryMeta(product.category); return (
{meta.label} · {getBrandLabel(product.brand)}

{product.name}

Vendor: {getVendorLabel(product.vendorId)}

${product.price} {product.rating.toFixed(1)} ★
{product.inStock ? In stock : Out} {product.freeShipping && ( Free ship )}
); } function jsonReplacer(_: string, value: unknown) { if (value instanceof Date) return value.toISOString(); return value; } function matchesFilter(cell: unknown, filter: { type: string; operator: string; values: Array }): boolean { if (filter.values.length === 0) return true; switch (filter.type) { case 'text': { const needle = String(filter.values[0] ?? '').toLowerCase(); return String(cell ?? '').toLowerCase().includes(needle); } case 'option': { return filter.values.includes(cell); } case 'multiOption': { const cellArr = Array.isArray(cell) ? cell : [cell]; return filter.values.some(v => cellArr.includes(v)); } case 'number': { const n = Number(cell); if (filter.operator === 'is between' || filter.operator === 'is not between') { const min = Number(filter.values[0]); const max = Number(filter.values[1]); const inside = n >= min && n <= max; return filter.operator === 'is between' ? inside : !inside; } return n === Number(filter.values[0]); } case 'date': { if (!(cell instanceof Date)) return false; const t = cell.getTime(); const from = filter.values[0] instanceof Date ? (filter.values[0] as Date).getTime() : Number.NEGATIVE_INFINITY; const to = filter.values[1] instanceof Date ? (filter.values[1] as Date).getTime() : from; return t >= from && t <= to; } case 'boolean': { return cell === filter.values[0]; } default: return true; } } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-data-table-side-filters ``` ## Usage `DataTableSideFilters` is built on top of the same `ColumnConfig` schema used by `DataTableFilter`, so a single column definition can power both the popover-style filter bar **and** the persistent side panel. The side panel emits its filter state as a `RuleGroupType` (the JSON shape `react-querybuilder` expects), so the same payload feeds the Query Builder, your saved-views layer, and your back end. ```tsx import { DataTableSideFilters, useDataTableSideFilters } from '@docyrus/ui/components/data-table-side-filters'; const columnsConfig = [ { id: 'category', displayName: 'Category', icon: ShoppingBagIcon, type: 'option' as const, accessor: (row: Product) => row.category, options: categoryOptions }, { id: 'tags', displayName: 'Features', icon: StarIcon, type: 'multiOption' as const, accessor: (row: Product) => row.tags, options: tagOptions }, { id: 'vendorId', displayName: 'Vendor', icon: UserIcon, type: 'multiOption' as const, accessor: (row: Product) => [row.vendorId], asyncOptions: { load: async ({ search, page, pageSize, signal }) => { const res = await fetch(`/api/vendors?q=${search}&page=${page}&size=${pageSize}`, { signal }); return res.json(); // → { items: ColumnOption[], hasMore: boolean } } } }, { id: 'price', displayName: 'Price', icon: DollarSignIcon, type: 'number' as const, accessor: (row: Product) => row.price, min: 0, max: 500 }, { id: 'releasedAt', displayName: 'Released', icon: CalendarIcon, type: 'date' as const, accessor: (row: Product) => row.releasedAt }, { id: 'inStock', displayName: 'In stock', icon: CircleCheckIcon, type: 'boolean' as const, accessor: (row: Product) => row.inStock } ] as const; function ProductFilters({ products }: { products: Array }) { const [query, setQuery] = useState({ combinator: 'and', rules: [] }); const { columns, filters, actions, strategy } = useDataTableSideFilters({ strategy: 'client', data: products, columnsConfig, query, onQueryChange: setQuery }); return ( ); } ``` ## Render mode resolution Each column picks its UI by `column.type`, with overrides via the `defaults` prop: | Column type | Default render mode | Notes | |-------------|---------------------|-------| | `text` | Text input | Debounced; auto-applies the `contains` operator | | `number` | Slider + Min/Max inputs | Validates `min ≤ max`, clamps to column `min`/`max` | | `date` | Date range picker | Quick presets (Today / Last 7 days / This month / etc.) | | `boolean` | Tri-state row (Any / True / False) | "Any" removes the filter | | `option` (≤ threshold) | Inline checkbox list | Shows faceted counts inline | | `option` (large list) | Inline checkbox list with **Show more** | Selected items stay pinned above the fold | | `multiOption` | Same as `option` | Add to/remove from the value list | | `option` / `multiOption` with `asyncOptions` | Dropdown popover with chips | Debounced server search + paginated load | Set `defaults[columnId].mode = 'dropdown-chips'` to force a long `option` list into a popover with a chip strip below the trigger — useful for `select`-style fields with many values that you don't want to inline. ## Sections, sticky filters & Show more Group filters into named sections with the `sections` prop, and pin "always-visible" filters above other sections via `defaults[id].sticky`: ```tsx ``` ## Query JSON output Whenever the user changes a value, the panel emits a `RuleGroupType` like: ```json { "combinator": "and", "rules": [ { "field": "category", "operator": "in", "value": ["audio", "wearables"] }, { "field": "price", "operator": "between", "value": [50, 250] }, { "field": "tags", "operator": "containsAll", "value": ["wireless", "noise-cancelling"] }, { "field": "vendorId", "operator": "in", "value": ["v-1", "v-3"] }, { "field": "releasedAt", "operator": "between", "value": ["2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z"] }, { "field": "inStock", "operator": "=", "value": true } ] } ``` This is the exact shape the [Query Builder](/docs/web/docyrus/query-builder) consumes. You can wire both components to the same `query` / `setQuery` state and let the user switch between e-commerce-style facet picking and rule-builder editing without translating data. Override the operator vocabulary with the `operatorMap` option if your back end expects different operator strings: ```tsx useDataTableSideFilters({ // ... operatorMap: { ...DEFAULT_OPERATOR_MAP, text: { contains: 'ilike', 'does not contain': 'not_ilike' } } }); ``` ## Features - **Field-aware UI** — text, number range, date range, boolean, options, multi-options, and async relations all render with the right control. - **Show-more pattern** — long option lists collapse to a configurable threshold; selected items stay pinned so they're never hidden. - **Chip strips for chosen values** — every popover-driven filter (relation, async, or large select) renders selected values as removable chips below the trigger. - **Built-in search input** — pass `searchable` to surface a debounced text search at the top of the panel. - **Sticky / collapsible / hidden sections** — fine-grained control via the `defaults` prop. - **Active-chip strip & clear actions** — global "Clear all" plus per-section "Clear" out of the box. - **QueryBuilder-compatible JSON** — emits `RuleGroupType` with operator translation; reverse translator rehydrates a side-panel from any saved query. - **Controlled & uncontrolled** — pass `defaultQuery` for uncontrolled, or `query` + `onQueryChange` for full external control. - **Reuses DataTableFilter operators & faceted helpers** — no duplicate operator metadata or counting logic. --- ## API Reference ### useDataTableSideFilters The main hook. Manages internal `FiltersState`, translates to/from `RuleGroupType`, and returns the values you pass into ``. | Prop | Type | Default | Description | |------|------|---------|-------------| | `strategy` | `'client' \| 'server'` | — | Filter strategy (mirrors `DataTableFilter`) | | `data` | `Array` | — | Row data (used for client-side faceting) | | `columnsConfig` | `ReadonlyArray>` | — | Column configuration array | | `defaultQuery` | `RuleGroupType` | — | Initial query (uncontrolled) | | `query` | `RuleGroupType` | — | Controlled query value | | `onQueryChange` | `(query: RuleGroupType) => void` | — | Emitted whenever the user changes a filter | | `combinator` | `'and' \| 'or'` | `'and'` | Top-level combinator on the emitted RuleGroup | | `operatorMap` | `SideFilterOperatorMap` | `DEFAULT_OPERATOR_MAP` | Override DTF→QB operator mapping | | `defaults` | `SideFilterDefaults` | — | Per-column UI hints | | `sections` | `ReadonlyArray` | — | Optional named groupings | | `options` | `Partial>` | — | Server-supplied options for `option`/`multiOption` columns | | `faceted` | `Partial>` | — | Server-supplied faceted counts / min-max tuples | | `calculateFacets` | `boolean` | `true` | Compute facets from `data` (turn off when supplying server-side) | **Returns:** | Field | Type | Description | |-------|------|-------------| | `columns` | `Array>` | Enriched column objects | | `filters` | `FiltersState` | Internal flat filter state | | `actions` | `DataTableFilterActions` | Same actions as `useDataTableFilters` | | `strategy` | `FilterStrategy` | Active strategy | | `query` | `RuleGroupType` | Currently emitted RuleGroup | | `defaults` | `SideFilterDefaults \| undefined` | Pass-through for the panel | | `sections` | `ReadonlyArray \| undefined` | Pass-through for the panel | | `reset` | `() => void` | Clear all filters | ### DataTableSideFilters | Prop | Type | Default | Description | |------|------|---------|-------------| | `columns` | `Array>` | — | From `useDataTableSideFilters` | | `filters` | `FiltersState` | — | From `useDataTableSideFilters` | | `actions` | `DataTableFilterActions` | — | From `useDataTableSideFilters` | | `strategy` | `FilterStrategy` | — | From `useDataTableSideFilters` | | `defaults` | `SideFilterDefaults` | — | Per-column UI hints | | `sections` | `ReadonlyArray` | — | Named groupings | | `locale` | `Locale` | `'en'` | i18n locale (shared with `DataTableFilter`) | | `title` | `ReactNode` | `'Filters'` | Panel header title | | `showActiveChips` | `boolean` | `true` | Show the active-filter chip strip | | `showClearAll` | `boolean` | `true` | Show "Clear all" in the header | | `searchable` | `boolean \| string` | `false` | Show a search input above the sections; pass a column id to bind to a specific text column | | `variant` | `'default' \| 'bordered' \| 'compact'` | `'default'` | Visual style | | `clearAllLabel` | `string` | `'Clear all'` | Label for the global clear button | | `clearLabel` | `string` | `'Clear'` | Label for per-section clear | | `className` | `string` | — | Extra classes on the panel root | ### SideFilterColumnDefaults | Field | Type | Description | |-------|------|-------------| | `mode` | `'auto' \| 'text-input' \| 'inline-checkbox' \| 'dropdown-chips' \| 'date' \| 'date-range' \| 'numeric-range' \| 'boolean'` | Force a specific render mode | | `showMoreThreshold` | `number` | Collapse inline checkbox list above N items (default `8`) | | `collapsed` | `boolean` | Section starts collapsed | | `sticky` | `boolean` | Pin section above all unpinned sections | | `title` | `ReactNode` | Override the section title (defaults to `column.displayName`) | | `hidden` | `boolean` | Hide the column from the panel completely | ### SideFilterSectionGroup | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Unique group id | | `title` | `ReactNode` | Group heading | | `columnIds` | `ReadonlyArray` | Column ids that belong to this group, in render order | | `withDivider` | `boolean` | Render a divider above the group title (default `true`) | --- ## Translator helpers Standalone utilities exposed for advanced use (e.g. saved-views, hydrating from URL): | Export | Signature | Description | |--------|-----------|-------------| | `filtersStateToRuleGroup` | `(state: FiltersState, combinator?, operatorMap?) => RuleGroupType` | Convert internal filters to a QueryBuilder rule group | | `ruleGroupToFiltersState` | `(group: RuleGroupType, columns, operatorMap?) => FiltersState` | Reverse: parse a rule group into internal filters (drops rules whose `field` doesn't match a known column) | | `DEFAULT_OPERATOR_MAP` | `SideFilterOperatorMap` | The default DTF → QB operator mapping (`contains`, `=`, `between`, `in`, etc.) | --- ## Sub-components The panel is composed of building blocks you can reach for directly when you need a custom layout. They're all exported from the same entry point: | Component | Description | |-----------|-------------| | `SideFilterSection` | Collapsible section wrapper used per column | | `SideFilterActiveChips` | Active-filter chip strip with per-chip remove | | `SideFilterSearch` | Debounced text-search bound to a specific text column | | `SideFilterClear` | "Clear all" button (shows count) | | `SideFilterText` | Single-column text input controller | | `SideFilterCheckboxList` | Inline checkbox list with "Show more" | | `SideFilterOptionsDropdown` | Static-options dropdown with chips | | `SideFilterAsyncOptions` | Async-loaded dropdown with chips | | `SideFilterDateRange` | Date-range picker with presets | | `SideFilterNumericRange` | Numeric range slider + min/max inputs with validation | | `SideFilterBoolean` | Tri-state radio (Any / True / False) | --- ## Type Exports | Type | Description | |------|-------------| | `DataTableSideFiltersProps` | Props for `` | | `DataTableSideFiltersOptions` | Options for `useDataTableSideFilters()` | | `UseDataTableSideFiltersReturn` | Return type of `useDataTableSideFilters()` | | `SideFilterColumnDefaults` | Per-column UI overrides | | `SideFilterDefaults` | `Record` | | `SideFilterSectionGroup` | Section grouping descriptor | | `SideFilterRenderMode` | All render-mode strings | | `SideFilterCombinator` | `'and' \| 'or'` | | `SideFilterOperatorMap` | DTF → QB operator override map |