Skip to content

Commit 289eca3

Browse files
authoredApr 24, 2024··
docs: start faceting docs and filter example reorg (#5507)
* docs: faceting docs and filter example reorg * update lock file
1 parent 365e0e9 commit 289eca3

28 files changed

+1297
-195
lines changed
 

‎docs/config.json

+13-5
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,19 @@
382382
},
383383
{
384384
"to": "framework/react/examples/column-groups",
385-
"label": "Column Groups"
385+
"label": "Header Groups"
386+
},
387+
{
388+
"to": "framework/react/examples/filters",
389+
"label": "Column Filters"
390+
},
391+
{
392+
"to": "framework/react/examples/filters-faceted",
393+
"label": "Column Filters (Faceted)"
394+
},
395+
{
396+
"to": "framework/react/examples/filters-fuzzy",
397+
"label": "Fuzzy Search Filters"
386398
},
387399
{
388400
"to": "framework/react/examples/column-ordering",
@@ -424,10 +436,6 @@
424436
"to": "framework/react/examples/sub-components",
425437
"label": "Sub Components"
426438
},
427-
{
428-
"to": "framework/react/examples/filters",
429-
"label": "Filters"
430-
},
431439
{
432440
"to": "framework/react/examples/fully-controlled",
433441
"label": "Fully Controlled"

‎docs/guide/column-faceting.md

+76-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,84 @@ title: Column Faceting Guide
66

77
Want to skip to the implementation? Check out these examples:
88

9-
- [filters](../../framework/react/examples/filters) (includes faceting)
9+
- [filters-faceted](../../framework/react/examples/filters-faceted)
1010

1111
## API
1212

1313
[Column Faceting API](../../api/features/column-faceting)
1414

15-
## Column Faceting Guide
15+
## Column Faceting Guide
16+
17+
Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component.
18+
19+
### Column Faceting Row Models
20+
21+
In order to use any of the column faceting features, you must include the appropriate row models in your table options.
22+
23+
```ts
24+
//only import the row models you need
25+
import {
26+
getCoreRowModel,
27+
getFacetedRowModel,
28+
getFacetedMinMaxValues, //depends on getFacetedRowModel
29+
getFacetedUniqueValues, //depends on getFacetedRowModel
30+
}
31+
//...
32+
const table = useReactTable({
33+
columns,
34+
data,
35+
getCoreRowModel: getCoreRowModel(),
36+
getFacetedRowModel: getFacetedRowModel(), //if you need a list of values for a column (other faceted row models depend on this one)
37+
getFacetedMinMaxValues: getFacetedMinMaxValues(), //if you need min/max values
38+
getFacetedUniqueValues: getFacetedUniqueValues(), //if you need a list of unique values
39+
//...
40+
})
41+
```
42+
43+
First, you must include the `getFacetedRowModel` row model. This row model will generate a list of values for a given column. If you need a list of unique values, include the `getFacetedUniqueValues` row model. If you need a tuple of minimum and maximum values, include the `getFacetedMinMaxValues` row model.
44+
45+
### Use Faceted Row Models
46+
47+
Once you have included the appropriate row models in your table options, you will be able to use the faceting column instance APIs to access the lists of values generated by the faceted row models.
48+
49+
```ts
50+
// list of unique values for autocomplete filter
51+
const autoCompleteSuggestions =
52+
Array.from(column.getFacetedUniqueValues().keys())
53+
.sort()
54+
.slice(0, 5000);
55+
```
56+
57+
```ts
58+
// tuple of min and max values for range filter
59+
const [min, max] = column.getFacetedMinMaxValues() ?? [0, 1];
60+
```
61+
62+
### Custom (Server-Side) Faceting
63+
64+
If instead of using the built-in client-side faceting features, you can implement your own faceting logic on the server-side and pass the faceted values to the client-side. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side.
65+
66+
```ts
67+
const facetingQuery = useQuery(
68+
//...
69+
)
70+
71+
const table = useReactTable({
72+
columns,
73+
data,
74+
getCoreRowModel: getCoreRowModel(),
75+
getFacetedRowModel: getFacetedRowModel(),
76+
getFacetedUniqueValues: (table, columnId) => {
77+
const uniqueValueMap = new Map<string, number>();
78+
//...
79+
return uniqueValueMap;
80+
},
81+
getFacetedMinMaxValues: (table, columnId) => {
82+
//...
83+
return [min, max];
84+
},
85+
//...
86+
})
87+
```
88+
89+
Alternatively, you don't have to put any of your faceting logic through the TanStack Table APIs at all. Just fetch your lists and pass them to your filter components directly.

‎docs/guide/column-filtering.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ title: Column Filtering Guide
77
Want to skip to the implementation? Check out these examples:
88

99
- [filters](../../framework/react/examples/filters) (includes faceting)
10+
- [filters-faceted](../../framework/react/examples/filters-faceted)
11+
- [filters-fuzzy](../../framework/react/examples/filters-fuzzy)
1012
- [editable-data](../../framework/react/examples/editable-data)
1113
- [expanding](../../framework/react/examples/expanding)
1214
- [grouping](../../framework/react/examples/grouping)

‎docs/guide/filters.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ The filter guides are now split into multiple guides:
99
- [Column Filtering](../column-filtering)
1010
- [Global Filtering](../global-filtering)
1111
- [Fuzzy Filtering](../fuzzy-filtering)
12-
- [Faceted Values](../faceted-values)
12+
- [Column Faceting](../column-faceting)
13+
- [Global Faceting](../global-faceting)

‎docs/guide/fuzzy-filtering.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Fuzzy Filtering Guide
66

77
Want to skip to the implementation? Check out these examples:
88

9-
- [filters](../../framework/react/examples/filters)
9+
- [filters-fuzzy](../../framework/react/examples/filters-fuzzy)
1010

1111
## API
1212

‎docs/guide/global-faceting.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Global Faceting Guide
66

77
Want to skip to the implementation? Check out these examples:
88

9-
- [filters](../../framework/react/examples/filters) (includes faceting)
9+
- [filters-faceted](../../framework/react/examples/filters)
1010

1111
## API
1212

‎docs/guide/global-filtering.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Global Filtering Guide
66

77
Want to skip to the implementation? Check out these examples:
88

9-
- [filters](../../framework/react/examples/filters) (includes faceting)
9+
- [filters-fuzzy](../../framework/react/examples/filters)
1010

1111
## API
1212

‎docs/guide/row-models.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ import {
4545
}
4646
//...
4747
const table = useReactTable({
48-
data,
4948
columns,
49+
data,
5050
getCoreRowModel: getCoreRowModel(),
5151
getExpandedRowModel: getExpandedRowModel(),
5252
getFacetedMinMaxValues: getFacetedMinMaxValues(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.DS_Store
3+
dist
4+
dist-ssr
5+
*.local
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm run start` or `yarn start`
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite App</title>
7+
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "tanstack-table-example-filters-faceted",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"serve": "vite preview --port 3001",
9+
"start": "vite"
10+
},
11+
"dependencies": {
12+
"@faker-js/faker": "^8.4.1",
13+
"@tanstack/match-sorter-utils": "^8.15.1",
14+
"@tanstack/react-table": "^8.16.0",
15+
"react": "^18.2.0",
16+
"react-dom": "^18.2.0"
17+
},
18+
"devDependencies": {
19+
"@rollup/plugin-replace": "^5.0.5",
20+
"@types/react": "^18.2.70",
21+
"@types/react-dom": "^18.2.22",
22+
"@vitejs/plugin-react": "^4.2.1",
23+
"typescript": "5.4.3",
24+
"vite": "^5.2.6"
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
html {
2+
font-family: sans-serif;
3+
font-size: 14px;
4+
}
5+
6+
table {
7+
border: 1px solid lightgray;
8+
}
9+
10+
tbody {
11+
border-bottom: 1px solid lightgray;
12+
}
13+
14+
th {
15+
border-bottom: 1px solid lightgray;
16+
border-right: 1px solid lightgray;
17+
padding: 2px 4px;
18+
}
19+
20+
tfoot {
21+
color: gray;
22+
}
23+
24+
tfoot th {
25+
font-weight: normal;
26+
}
+366
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
4+
import './index.css'
5+
6+
import {
7+
Column,
8+
ColumnDef,
9+
ColumnFiltersState,
10+
RowData,
11+
flexRender,
12+
getCoreRowModel,
13+
getFacetedMinMaxValues,
14+
getFacetedRowModel,
15+
getFacetedUniqueValues,
16+
getFilteredRowModel,
17+
getPaginationRowModel,
18+
getSortedRowModel,
19+
useReactTable,
20+
} from '@tanstack/react-table'
21+
22+
import { makeData, Person } from './makeData'
23+
24+
declare module '@tanstack/react-table' {
25+
//allows us to define custom properties for our columns
26+
interface ColumnMeta<TData extends RowData, TValue> {
27+
filterVariant?: 'text' | 'range' | 'select'
28+
}
29+
}
30+
31+
function App() {
32+
const rerender = React.useReducer(() => ({}), {})[1]
33+
34+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
35+
[]
36+
)
37+
38+
const columns = React.useMemo<ColumnDef<Person, any>[]>(
39+
() => [
40+
{
41+
accessorKey: 'firstName',
42+
cell: info => info.getValue(),
43+
},
44+
{
45+
accessorFn: row => row.lastName,
46+
id: 'lastName',
47+
cell: info => info.getValue(),
48+
header: () => <span>Last Name</span>,
49+
},
50+
{
51+
accessorKey: 'age',
52+
header: () => 'Age',
53+
meta: {
54+
filterVariant: 'range',
55+
},
56+
},
57+
{
58+
accessorKey: 'visits',
59+
header: () => <span>Visits</span>,
60+
meta: {
61+
filterVariant: 'range',
62+
},
63+
},
64+
{
65+
accessorKey: 'status',
66+
header: 'Status',
67+
meta: {
68+
filterVariant: 'select',
69+
},
70+
},
71+
{
72+
accessorKey: 'progress',
73+
header: 'Profile Progress',
74+
meta: {
75+
filterVariant: 'range',
76+
},
77+
},
78+
],
79+
[]
80+
)
81+
82+
const [data, setData] = React.useState<Person[]>(() => makeData(5_000))
83+
const refreshData = () => setData(_old => makeData(100_000)) //stress test
84+
85+
const table = useReactTable({
86+
data,
87+
columns,
88+
state: {
89+
columnFilters,
90+
},
91+
onColumnFiltersChange: setColumnFilters,
92+
getCoreRowModel: getCoreRowModel(),
93+
getFilteredRowModel: getFilteredRowModel(), //client-side filtering
94+
getSortedRowModel: getSortedRowModel(),
95+
getPaginationRowModel: getPaginationRowModel(),
96+
getFacetedRowModel: getFacetedRowModel(), // client-side faceting
97+
getFacetedUniqueValues: getFacetedUniqueValues(), // generate unique values for select filter/autocomplete
98+
getFacetedMinMaxValues: getFacetedMinMaxValues(), // generate min/max values for range filter
99+
debugTable: true,
100+
debugHeaders: true,
101+
debugColumns: false,
102+
})
103+
104+
return (
105+
<div className="p-2">
106+
<table>
107+
<thead>
108+
{table.getHeaderGroups().map(headerGroup => (
109+
<tr key={headerGroup.id}>
110+
{headerGroup.headers.map(header => {
111+
return (
112+
<th key={header.id} colSpan={header.colSpan}>
113+
{header.isPlaceholder ? null : (
114+
<>
115+
<div
116+
{...{
117+
className: header.column.getCanSort()
118+
? 'cursor-pointer select-none'
119+
: '',
120+
onClick: header.column.getToggleSortingHandler(),
121+
}}
122+
>
123+
{flexRender(
124+
header.column.columnDef.header,
125+
header.getContext()
126+
)}
127+
{{
128+
asc: ' 🔼',
129+
desc: ' 🔽',
130+
}[header.column.getIsSorted() as string] ?? null}
131+
</div>
132+
{header.column.getCanFilter() ? (
133+
<div>
134+
<Filter column={header.column} />
135+
</div>
136+
) : null}
137+
</>
138+
)}
139+
</th>
140+
)
141+
})}
142+
</tr>
143+
))}
144+
</thead>
145+
<tbody>
146+
{table.getRowModel().rows.map(row => {
147+
return (
148+
<tr key={row.id}>
149+
{row.getVisibleCells().map(cell => {
150+
return (
151+
<td key={cell.id}>
152+
{flexRender(
153+
cell.column.columnDef.cell,
154+
cell.getContext()
155+
)}
156+
</td>
157+
)
158+
})}
159+
</tr>
160+
)
161+
})}
162+
</tbody>
163+
</table>
164+
<div className="h-2" />
165+
<div className="flex items-center gap-2">
166+
<button
167+
className="border rounded p-1"
168+
onClick={() => table.setPageIndex(0)}
169+
disabled={!table.getCanPreviousPage()}
170+
>
171+
{'<<'}
172+
</button>
173+
<button
174+
className="border rounded p-1"
175+
onClick={() => table.previousPage()}
176+
disabled={!table.getCanPreviousPage()}
177+
>
178+
{'<'}
179+
</button>
180+
<button
181+
className="border rounded p-1"
182+
onClick={() => table.nextPage()}
183+
disabled={!table.getCanNextPage()}
184+
>
185+
{'>'}
186+
</button>
187+
<button
188+
className="border rounded p-1"
189+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
190+
disabled={!table.getCanNextPage()}
191+
>
192+
{'>>'}
193+
</button>
194+
<span className="flex items-center gap-1">
195+
<div>Page</div>
196+
<strong>
197+
{table.getState().pagination.pageIndex + 1} of{' '}
198+
{table.getPageCount()}
199+
</strong>
200+
</span>
201+
<span className="flex items-center gap-1">
202+
| Go to page:
203+
<input
204+
type="number"
205+
defaultValue={table.getState().pagination.pageIndex + 1}
206+
onChange={e => {
207+
const page = e.target.value ? Number(e.target.value) - 1 : 0
208+
table.setPageIndex(page)
209+
}}
210+
className="border p-1 rounded w-16"
211+
/>
212+
</span>
213+
<select
214+
value={table.getState().pagination.pageSize}
215+
onChange={e => {
216+
table.setPageSize(Number(e.target.value))
217+
}}
218+
>
219+
{[10, 20, 30, 40, 50].map(pageSize => (
220+
<option key={pageSize} value={pageSize}>
221+
Show {pageSize}
222+
</option>
223+
))}
224+
</select>
225+
</div>
226+
<div>{table.getPrePaginationRowModel().rows.length} Rows</div>
227+
<div>
228+
<button onClick={() => rerender()}>Force Rerender</button>
229+
</div>
230+
<div>
231+
<button onClick={() => refreshData()}>Refresh Data</button>
232+
</div>
233+
<pre>
234+
{JSON.stringify(
235+
{ columnFilters: table.getState().columnFilters },
236+
null,
237+
2
238+
)}
239+
</pre>
240+
</div>
241+
)
242+
}
243+
244+
function Filter({ column }: { column: Column<any, unknown> }) {
245+
const { filterVariant } = column.columnDef.meta ?? {}
246+
247+
const columnFilterValue = column.getFilterValue()
248+
249+
const sortedUniqueValues = React.useMemo(
250+
() =>
251+
filterVariant === 'range'
252+
? []
253+
: Array.from(column.getFacetedUniqueValues().keys())
254+
.sort()
255+
.slice(0, 5000),
256+
[column.getFacetedUniqueValues(), filterVariant]
257+
)
258+
259+
return filterVariant === 'range' ? (
260+
<div>
261+
<div className="flex space-x-2">
262+
<DebouncedInput
263+
type="number"
264+
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
265+
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
266+
value={(columnFilterValue as [number, number])?.[0] ?? ''}
267+
onChange={value =>
268+
column.setFilterValue((old: [number, number]) => [value, old?.[1]])
269+
}
270+
placeholder={`Min ${
271+
column.getFacetedMinMaxValues()?.[0] !== undefined
272+
? `(${column.getFacetedMinMaxValues()?.[0]})`
273+
: ''
274+
}`}
275+
className="w-24 border shadow rounded"
276+
/>
277+
<DebouncedInput
278+
type="number"
279+
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
280+
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
281+
value={(columnFilterValue as [number, number])?.[1] ?? ''}
282+
onChange={value =>
283+
column.setFilterValue((old: [number, number]) => [old?.[0], value])
284+
}
285+
placeholder={`Max ${
286+
column.getFacetedMinMaxValues()?.[1]
287+
? `(${column.getFacetedMinMaxValues()?.[1]})`
288+
: ''
289+
}`}
290+
className="w-24 border shadow rounded"
291+
/>
292+
</div>
293+
<div className="h-1" />
294+
</div>
295+
) : filterVariant === 'select' ? (
296+
<select
297+
onChange={e => column.setFilterValue(e.target.value)}
298+
value={columnFilterValue?.toString()}
299+
>
300+
<option value="">All</option>
301+
{sortedUniqueValues.map(value => (
302+
//dynamically generated select options from faceted values feature
303+
<option value={value} key={value}>
304+
{value}
305+
</option>
306+
))}
307+
</select>
308+
) : (
309+
<>
310+
{/* Autocomplete suggestions from faceted values feature */}
311+
<datalist id={column.id + 'list'}>
312+
{sortedUniqueValues.map((value: any) => (
313+
<option value={value} key={value} />
314+
))}
315+
</datalist>
316+
<DebouncedInput
317+
type="text"
318+
value={(columnFilterValue ?? '') as string}
319+
onChange={value => column.setFilterValue(value)}
320+
placeholder={`Search... (${column.getFacetedUniqueValues().size})`}
321+
className="w-36 border shadow rounded"
322+
list={column.id + 'list'}
323+
/>
324+
<div className="h-1" />
325+
</>
326+
)
327+
}
328+
329+
// A typical debounced input react component
330+
function DebouncedInput({
331+
value: initialValue,
332+
onChange,
333+
debounce = 500,
334+
...props
335+
}: {
336+
value: string | number
337+
onChange: (value: string | number) => void
338+
debounce?: number
339+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
340+
const [value, setValue] = React.useState(initialValue)
341+
342+
React.useEffect(() => {
343+
setValue(initialValue)
344+
}, [initialValue])
345+
346+
React.useEffect(() => {
347+
const timeout = setTimeout(() => {
348+
onChange(value)
349+
}, debounce)
350+
351+
return () => clearTimeout(timeout)
352+
}, [value])
353+
354+
return (
355+
<input {...props} value={value} onChange={e => setValue(e.target.value)} />
356+
)
357+
}
358+
359+
const rootElement = document.getElementById('root')
360+
if (!rootElement) throw new Error('Failed to find the root element')
361+
362+
ReactDOM.createRoot(rootElement).render(
363+
<React.StrictMode>
364+
<App />
365+
</React.StrictMode>
366+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { faker } from '@faker-js/faker'
2+
3+
export type Person = {
4+
firstName: string
5+
lastName: string
6+
age: number
7+
visits: number
8+
progress: number
9+
status: 'relationship' | 'complicated' | 'single'
10+
subRows?: Person[]
11+
}
12+
13+
const range = (len: number) => {
14+
const arr: number[] = []
15+
for (let i = 0; i < len; i++) {
16+
arr.push(i)
17+
}
18+
return arr
19+
}
20+
21+
const newPerson = (): Person => {
22+
return {
23+
firstName: faker.person.firstName(),
24+
lastName: faker.person.lastName(),
25+
age: faker.number.int(40),
26+
visits: faker.number.int(1000),
27+
progress: faker.number.int(100),
28+
status: faker.helpers.shuffle<Person['status']>([
29+
'relationship',
30+
'complicated',
31+
'single',
32+
])[0]!,
33+
}
34+
}
35+
36+
export function makeData(...lens: number[]) {
37+
const makeDataLevel = (depth = 0): Person[] => {
38+
const len = lens[depth]!
39+
return range(len).map((d): Person => {
40+
return {
41+
...newPerson(),
42+
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
43+
}
44+
})
45+
}
46+
47+
return makeDataLevel()
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"useDefineForClassFields": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
"module": "ESNext",
7+
"skipLibCheck": true,
8+
9+
/* Bundler mode */
10+
"moduleResolution": "bundler",
11+
"allowImportingTsExtensions": true,
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"noEmit": true,
15+
"jsx": "react-jsx",
16+
17+
/* Linting */
18+
"strict": true,
19+
"noUnusedLocals": true,
20+
"noUnusedParameters": true,
21+
"noFallthroughCasesInSwitch": true
22+
},
23+
"include": ["src"]
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
import rollupReplace from '@rollup/plugin-replace'
4+
5+
// https://vitejs.dev/config/
6+
export default defineConfig({
7+
plugins: [
8+
rollupReplace({
9+
preventAssignment: true,
10+
values: {
11+
__DEV__: JSON.stringify(true),
12+
'process.env.NODE_ENV': JSON.stringify('development'),
13+
},
14+
}),
15+
react(),
16+
],
17+
})
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.DS_Store
3+
dist
4+
dist-ssr
5+
*.local
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm run start` or `yarn start`
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite App</title>
7+
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "tanstack-table-example-filters-fuzzy",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"serve": "vite preview --port 3001",
9+
"start": "vite"
10+
},
11+
"dependencies": {
12+
"@faker-js/faker": "^8.4.1",
13+
"@tanstack/match-sorter-utils": "^8.15.1",
14+
"@tanstack/react-table": "^8.16.0",
15+
"react": "^18.2.0",
16+
"react-dom": "^18.2.0"
17+
},
18+
"devDependencies": {
19+
"@rollup/plugin-replace": "^5.0.5",
20+
"@types/react": "^18.2.70",
21+
"@types/react-dom": "^18.2.22",
22+
"@vitejs/plugin-react": "^4.2.1",
23+
"typescript": "5.4.3",
24+
"vite": "^5.2.6"
25+
}
26+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
html {
2+
font-family: sans-serif;
3+
font-size: 14px;
4+
}
5+
6+
table {
7+
border: 1px solid lightgray;
8+
}
9+
10+
tbody {
11+
border-bottom: 1px solid lightgray;
12+
}
13+
14+
th {
15+
border-bottom: 1px solid lightgray;
16+
border-right: 1px solid lightgray;
17+
padding: 2px 4px;
18+
}
19+
20+
tfoot {
21+
color: gray;
22+
}
23+
24+
tfoot th {
25+
font-weight: normal;
26+
}
+346
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
4+
import './index.css'
5+
6+
import {
7+
Column,
8+
ColumnDef,
9+
ColumnFiltersState,
10+
FilterFn,
11+
SortingFn,
12+
flexRender,
13+
getCoreRowModel,
14+
getFilteredRowModel,
15+
getPaginationRowModel,
16+
getSortedRowModel,
17+
sortingFns,
18+
useReactTable,
19+
} from '@tanstack/react-table'
20+
21+
// A TanStack fork of Kent C. Dodds' match-sorter library that provides ranking information
22+
import {
23+
RankingInfo,
24+
rankItem,
25+
compareItems,
26+
} from '@tanstack/match-sorter-utils'
27+
28+
import { makeData, Person } from './makeData'
29+
30+
declare module '@tanstack/react-table' {
31+
//add fuzzy filter to the filterFns
32+
interface FilterFns {
33+
fuzzy: FilterFn<unknown>
34+
}
35+
interface FilterMeta {
36+
itemRank: RankingInfo
37+
}
38+
}
39+
40+
// Define a custom fuzzy filter function that will apply ranking info to rows (using match-sorter utils)
41+
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
42+
// Rank the item
43+
const itemRank = rankItem(row.getValue(columnId), value)
44+
45+
// Store the itemRank info
46+
addMeta({
47+
itemRank,
48+
})
49+
50+
// Return if the item should be filtered in/out
51+
return itemRank.passed
52+
}
53+
54+
// Define a custom fuzzy sort function that will sort by rank if the row has ranking information
55+
const fuzzySort: SortingFn<any> = (rowA, rowB, columnId) => {
56+
let dir = 0
57+
58+
// Only sort by rank if the column has ranking information
59+
if (rowA.columnFiltersMeta[columnId]) {
60+
dir = compareItems(
61+
rowA.columnFiltersMeta[columnId]?.itemRank!,
62+
rowB.columnFiltersMeta[columnId]?.itemRank!
63+
)
64+
}
65+
66+
// Provide an alphanumeric fallback for when the item ranks are equal
67+
return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir
68+
}
69+
70+
function App() {
71+
const rerender = React.useReducer(() => ({}), {})[1]
72+
73+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
74+
[]
75+
)
76+
const [globalFilter, setGlobalFilter] = React.useState('')
77+
78+
const columns = React.useMemo<ColumnDef<Person, any>[]>(
79+
() => [
80+
{
81+
accessorKey: 'id',
82+
filterFn: 'equalsString', //note: normal non-fuzzy filter column - exact match required
83+
},
84+
{
85+
accessorKey: 'firstName',
86+
cell: info => info.getValue(),
87+
filterFn: 'includesStringSensitive', //note: normal non-fuzzy filter column
88+
},
89+
{
90+
accessorFn: row => row.lastName, //note: normal non-fuzzy filter column - case sensitive
91+
id: 'lastName',
92+
cell: info => info.getValue(),
93+
header: () => <span>Last Name</span>,
94+
filterFn: 'includesString', //note: normal non-fuzzy filter column - case insensitive
95+
},
96+
{
97+
accessorFn: row => `${row.firstName} ${row.lastName}`,
98+
id: 'fullName',
99+
header: 'Full Name',
100+
cell: info => info.getValue(),
101+
filterFn: 'fuzzy', //using our custom fuzzy filter function
102+
// filterFn: fuzzyFilter, //or just define with the function
103+
sortingFn: fuzzySort, //sort by fuzzy rank (falls back to alphanumeric)
104+
},
105+
],
106+
[]
107+
)
108+
109+
const [data, setData] = React.useState<Person[]>(() => makeData(5_000))
110+
const refreshData = () => setData(_old => makeData(50_000)) //stress test
111+
112+
const table = useReactTable({
113+
data,
114+
columns,
115+
filterFns: {
116+
fuzzy: fuzzyFilter, //define as a filter function that can be used in column definitions
117+
},
118+
state: {
119+
columnFilters,
120+
globalFilter,
121+
},
122+
onColumnFiltersChange: setColumnFilters,
123+
onGlobalFilterChange: setGlobalFilter,
124+
globalFilterFn: 'fuzzy', //apply fuzzy filter to the global filter (most common use case for fuzzy filter)
125+
getCoreRowModel: getCoreRowModel(),
126+
getFilteredRowModel: getFilteredRowModel(), //client side filtering
127+
getSortedRowModel: getSortedRowModel(),
128+
getPaginationRowModel: getPaginationRowModel(),
129+
debugTable: true,
130+
debugHeaders: true,
131+
debugColumns: false,
132+
})
133+
134+
//apply the fuzzy sort if the fullName column is being filtered
135+
React.useEffect(() => {
136+
if (table.getState().columnFilters[0]?.id === 'fullName') {
137+
if (table.getState().sorting[0]?.id !== 'fullName') {
138+
table.setSorting([{ id: 'fullName', desc: false }])
139+
}
140+
}
141+
}, [table.getState().columnFilters[0]?.id])
142+
143+
return (
144+
<div className="p-2">
145+
<div>
146+
<DebouncedInput
147+
value={globalFilter ?? ''}
148+
onChange={value => setGlobalFilter(String(value))}
149+
className="p-2 font-lg shadow border border-block"
150+
placeholder="Search all columns..."
151+
/>
152+
</div>
153+
<div className="h-2" />
154+
<table>
155+
<thead>
156+
{table.getHeaderGroups().map(headerGroup => (
157+
<tr key={headerGroup.id}>
158+
{headerGroup.headers.map(header => {
159+
return (
160+
<th key={header.id} colSpan={header.colSpan}>
161+
{header.isPlaceholder ? null : (
162+
<>
163+
<div
164+
{...{
165+
className: header.column.getCanSort()
166+
? 'cursor-pointer select-none'
167+
: '',
168+
onClick: header.column.getToggleSortingHandler(),
169+
}}
170+
>
171+
{flexRender(
172+
header.column.columnDef.header,
173+
header.getContext()
174+
)}
175+
{{
176+
asc: ' 🔼',
177+
desc: ' 🔽',
178+
}[header.column.getIsSorted() as string] ?? null}
179+
</div>
180+
{header.column.getCanFilter() ? (
181+
<div>
182+
<Filter column={header.column} />
183+
</div>
184+
) : null}
185+
</>
186+
)}
187+
</th>
188+
)
189+
})}
190+
</tr>
191+
))}
192+
</thead>
193+
<tbody>
194+
{table.getRowModel().rows.map(row => {
195+
return (
196+
<tr key={row.id}>
197+
{row.getVisibleCells().map(cell => {
198+
return (
199+
<td key={cell.id}>
200+
{flexRender(
201+
cell.column.columnDef.cell,
202+
cell.getContext()
203+
)}
204+
</td>
205+
)
206+
})}
207+
</tr>
208+
)
209+
})}
210+
</tbody>
211+
</table>
212+
<div className="h-2" />
213+
<div className="flex items-center gap-2">
214+
<button
215+
className="border rounded p-1"
216+
onClick={() => table.setPageIndex(0)}
217+
disabled={!table.getCanPreviousPage()}
218+
>
219+
{'<<'}
220+
</button>
221+
<button
222+
className="border rounded p-1"
223+
onClick={() => table.previousPage()}
224+
disabled={!table.getCanPreviousPage()}
225+
>
226+
{'<'}
227+
</button>
228+
<button
229+
className="border rounded p-1"
230+
onClick={() => table.nextPage()}
231+
disabled={!table.getCanNextPage()}
232+
>
233+
{'>'}
234+
</button>
235+
<button
236+
className="border rounded p-1"
237+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
238+
disabled={!table.getCanNextPage()}
239+
>
240+
{'>>'}
241+
</button>
242+
<span className="flex items-center gap-1">
243+
<div>Page</div>
244+
<strong>
245+
{table.getState().pagination.pageIndex + 1} of{' '}
246+
{table.getPageCount()}
247+
</strong>
248+
</span>
249+
<span className="flex items-center gap-1">
250+
| Go to page:
251+
<input
252+
type="number"
253+
defaultValue={table.getState().pagination.pageIndex + 1}
254+
onChange={e => {
255+
const page = e.target.value ? Number(e.target.value) - 1 : 0
256+
table.setPageIndex(page)
257+
}}
258+
className="border p-1 rounded w-16"
259+
/>
260+
</span>
261+
<select
262+
value={table.getState().pagination.pageSize}
263+
onChange={e => {
264+
table.setPageSize(Number(e.target.value))
265+
}}
266+
>
267+
{[10, 20, 30, 40, 50].map(pageSize => (
268+
<option key={pageSize} value={pageSize}>
269+
Show {pageSize}
270+
</option>
271+
))}
272+
</select>
273+
</div>
274+
<div>{table.getPrePaginationRowModel().rows.length} Rows</div>
275+
<div>
276+
<button onClick={() => rerender()}>Force Rerender</button>
277+
</div>
278+
<div>
279+
<button onClick={() => refreshData()}>Refresh Data</button>
280+
</div>
281+
<pre>
282+
{JSON.stringify(
283+
{
284+
columnFilters: table.getState().columnFilters,
285+
globalFilter: table.getState().globalFilter,
286+
},
287+
null,
288+
2
289+
)}
290+
</pre>
291+
</div>
292+
)
293+
}
294+
295+
function Filter({ column }: { column: Column<any, unknown> }) {
296+
const columnFilterValue = column.getFilterValue()
297+
298+
return (
299+
<DebouncedInput
300+
type="text"
301+
value={(columnFilterValue ?? '') as string}
302+
onChange={value => column.setFilterValue(value)}
303+
placeholder={`Search...`}
304+
className="w-36 border shadow rounded"
305+
/>
306+
)
307+
}
308+
309+
// A typical debounced input react component
310+
function DebouncedInput({
311+
value: initialValue,
312+
onChange,
313+
debounce = 500,
314+
...props
315+
}: {
316+
value: string | number
317+
onChange: (value: string | number) => void
318+
debounce?: number
319+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
320+
const [value, setValue] = React.useState(initialValue)
321+
322+
React.useEffect(() => {
323+
setValue(initialValue)
324+
}, [initialValue])
325+
326+
React.useEffect(() => {
327+
const timeout = setTimeout(() => {
328+
onChange(value)
329+
}, debounce)
330+
331+
return () => clearTimeout(timeout)
332+
}, [value])
333+
334+
return (
335+
<input {...props} value={value} onChange={e => setValue(e.target.value)} />
336+
)
337+
}
338+
339+
const rootElement = document.getElementById('root')
340+
if (!rootElement) throw new Error('Failed to find the root element')
341+
342+
ReactDOM.createRoot(rootElement).render(
343+
<React.StrictMode>
344+
<App />
345+
</React.StrictMode>
346+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { faker } from '@faker-js/faker'
2+
3+
export type Person = {
4+
id: number
5+
firstName: string
6+
lastName: string
7+
age: number
8+
visits: number
9+
progress: number
10+
status: 'relationship' | 'complicated' | 'single'
11+
subRows?: Person[]
12+
}
13+
14+
const range = (len: number) => {
15+
const arr: number[] = []
16+
for (let i = 0; i < len; i++) {
17+
arr.push(i)
18+
}
19+
return arr
20+
}
21+
22+
const newPerson = (num: number): Person => {
23+
return {
24+
id: num,
25+
firstName: faker.person.firstName(),
26+
lastName: faker.person.lastName(),
27+
age: faker.number.int(40),
28+
visits: faker.number.int(1000),
29+
progress: faker.number.int(100),
30+
status: faker.helpers.shuffle<Person['status']>([
31+
'relationship',
32+
'complicated',
33+
'single',
34+
])[0]!,
35+
}
36+
}
37+
38+
export function makeData(...lens: number[]) {
39+
const makeDataLevel = (depth = 0): Person[] => {
40+
const len = lens[depth]!
41+
return range(len).map((index): Person => {
42+
return {
43+
...newPerson(index),
44+
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
45+
}
46+
})
47+
}
48+
49+
return makeDataLevel()
50+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"useDefineForClassFields": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
"module": "ESNext",
7+
"skipLibCheck": true,
8+
9+
/* Bundler mode */
10+
"moduleResolution": "bundler",
11+
"allowImportingTsExtensions": true,
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"noEmit": true,
15+
"jsx": "react-jsx",
16+
17+
/* Linting */
18+
"strict": true,
19+
"noUnusedLocals": true,
20+
"noUnusedParameters": true,
21+
"noFallthroughCasesInSwitch": true
22+
},
23+
"include": ["src"]
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
import rollupReplace from '@rollup/plugin-replace'
4+
5+
// https://vitejs.dev/config/
6+
export default defineConfig({
7+
plugins: [
8+
rollupReplace({
9+
preventAssignment: true,
10+
values: {
11+
__DEV__: JSON.stringify(true),
12+
'process.env.NODE_ENV': JSON.stringify('development'),
13+
},
14+
}),
15+
react(),
16+
],
17+
})

‎examples/react/filters/src/main.tsx

+82-183
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,22 @@ import {
77
Column,
88
ColumnDef,
99
ColumnFiltersState,
10-
FilterFn,
11-
SortingFn,
12-
Table,
10+
RowData,
1311
flexRender,
1412
getCoreRowModel,
15-
getFacetedMinMaxValues,
16-
getFacetedRowModel,
17-
getFacetedUniqueValues,
1813
getFilteredRowModel,
1914
getPaginationRowModel,
2015
getSortedRowModel,
21-
sortingFns,
2216
useReactTable,
2317
} from '@tanstack/react-table'
2418

25-
import {
26-
RankingInfo,
27-
rankItem,
28-
compareItems,
29-
} from '@tanstack/match-sorter-utils'
30-
3119
import { makeData, Person } from './makeData'
3220

3321
declare module '@tanstack/react-table' {
34-
interface FilterFns {
35-
fuzzy: FilterFn<unknown>
36-
}
37-
interface FilterMeta {
38-
itemRank: RankingInfo
39-
}
40-
}
41-
42-
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
43-
// Rank the item
44-
const itemRank = rankItem(row.getValue(columnId), value)
45-
46-
// Store the itemRank info
47-
addMeta({
48-
itemRank,
49-
})
50-
51-
// Return if the item should be filtered in/out
52-
return itemRank.passed
53-
}
54-
55-
const fuzzySort: SortingFn<any> = (rowA, rowB, columnId) => {
56-
let dir = 0
57-
58-
// Only sort by rank if the column has ranking information
59-
if (rowA.columnFiltersMeta[columnId]) {
60-
dir = compareItems(
61-
rowA.columnFiltersMeta[columnId]?.itemRank!,
62-
rowB.columnFiltersMeta[columnId]?.itemRank!
63-
)
22+
//allows us to define custom properties for our columns
23+
interface ColumnMeta<TData extends RowData, TValue> {
24+
filterVariant?: 'text' | 'range' | 'select'
6425
}
65-
66-
// Provide an alphanumeric fallback for when the item ranks are equal
67-
return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir
6826
}
6927

7028
function App() {
@@ -73,119 +31,79 @@ function App() {
7331
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
7432
[]
7533
)
76-
const [globalFilter, setGlobalFilter] = React.useState('')
7734

7835
const columns = React.useMemo<ColumnDef<Person, any>[]>(
7936
() => [
8037
{
81-
header: 'Name',
82-
footer: props => props.column.id,
83-
columns: [
84-
{
85-
accessorKey: 'firstName',
86-
cell: info => info.getValue(),
87-
footer: props => props.column.id,
88-
},
89-
{
90-
accessorFn: row => row.lastName,
91-
id: 'lastName',
92-
cell: info => info.getValue(),
93-
header: () => <span>Last Name</span>,
94-
footer: props => props.column.id,
95-
},
96-
{
97-
accessorFn: row => `${row.firstName} ${row.lastName}`,
98-
id: 'fullName',
99-
header: 'Full Name',
100-
cell: info => info.getValue(),
101-
footer: props => props.column.id,
102-
filterFn: 'fuzzy',
103-
sortingFn: fuzzySort,
104-
},
105-
],
38+
accessorKey: 'firstName',
39+
cell: info => info.getValue(),
40+
},
41+
{
42+
accessorFn: row => row.lastName,
43+
id: 'lastName',
44+
cell: info => info.getValue(),
45+
header: () => <span>Last Name</span>,
46+
},
47+
{
48+
accessorFn: row => `${row.firstName} ${row.lastName}`,
49+
id: 'fullName',
50+
header: 'Full Name',
51+
cell: info => info.getValue(),
52+
},
53+
{
54+
accessorKey: 'age',
55+
header: () => 'Age',
56+
meta: {
57+
filterVariant: 'range',
58+
},
59+
},
60+
{
61+
accessorKey: 'visits',
62+
header: () => <span>Visits</span>,
63+
meta: {
64+
filterVariant: 'range',
65+
},
66+
},
67+
{
68+
accessorKey: 'status',
69+
header: 'Status',
70+
meta: {
71+
filterVariant: 'select',
72+
},
10673
},
10774
{
108-
header: 'Info',
109-
footer: props => props.column.id,
110-
columns: [
111-
{
112-
accessorKey: 'age',
113-
header: () => 'Age',
114-
footer: props => props.column.id,
115-
},
116-
{
117-
header: 'More Info',
118-
columns: [
119-
{
120-
accessorKey: 'visits',
121-
header: () => <span>Visits</span>,
122-
footer: props => props.column.id,
123-
},
124-
{
125-
accessorKey: 'status',
126-
header: 'Status',
127-
footer: props => props.column.id,
128-
},
129-
{
130-
accessorKey: 'progress',
131-
header: 'Profile Progress',
132-
footer: props => props.column.id,
133-
},
134-
],
135-
},
136-
],
75+
accessorKey: 'progress',
76+
header: 'Profile Progress',
77+
meta: {
78+
filterVariant: 'range',
79+
},
13780
},
13881
],
13982
[]
14083
)
14184

142-
const [data, setData] = React.useState<Person[]>(() => makeData(50000))
143-
const refreshData = () => setData(_old => makeData(50000))
85+
const [data, setData] = React.useState<Person[]>(() => makeData(5_000))
86+
const refreshData = () => setData(_old => makeData(50_000)) //stress test
14487

14588
const table = useReactTable({
14689
data,
14790
columns,
148-
filterFns: {
149-
fuzzy: fuzzyFilter,
150-
},
91+
filterFns: {},
15192
state: {
15293
columnFilters,
153-
globalFilter,
15494
},
15595
onColumnFiltersChange: setColumnFilters,
156-
onGlobalFilterChange: setGlobalFilter,
157-
globalFilterFn: fuzzyFilter,
15896
getCoreRowModel: getCoreRowModel(),
159-
getFilteredRowModel: getFilteredRowModel(),
97+
getFilteredRowModel: getFilteredRowModel(), //client side filtering
16098
getSortedRowModel: getSortedRowModel(),
16199
getPaginationRowModel: getPaginationRowModel(),
162-
getFacetedRowModel: getFacetedRowModel(),
163-
getFacetedUniqueValues: getFacetedUniqueValues(),
164-
getFacetedMinMaxValues: getFacetedMinMaxValues(),
165100
debugTable: true,
166101
debugHeaders: true,
167102
debugColumns: false,
168103
})
169104

170-
React.useEffect(() => {
171-
if (table.getState().columnFilters[0]?.id === 'fullName') {
172-
if (table.getState().sorting[0]?.id !== 'fullName') {
173-
table.setSorting([{ id: 'fullName', desc: false }])
174-
}
175-
}
176-
}, [table.getState().columnFilters[0]?.id])
177-
178105
return (
179106
<div className="p-2">
180-
<div>
181-
<DebouncedInput
182-
value={globalFilter ?? ''}
183-
onChange={value => setGlobalFilter(String(value))}
184-
className="p-2 font-lg shadow border border-block"
185-
placeholder="Search all columns..."
186-
/>
187-
</div>
188-
<div className="h-2" />
189107
<table>
190108
<thead>
191109
{table.getHeaderGroups().map(headerGroup => (
@@ -214,7 +132,7 @@ function App() {
214132
</div>
215133
{header.column.getCanFilter() ? (
216134
<div>
217-
<Filter column={header.column} table={table} />
135+
<Filter column={header.column} />
218136
</div>
219137
) : null}
220138
</>
@@ -313,89 +231,70 @@ function App() {
313231
<div>
314232
<button onClick={() => refreshData()}>Refresh Data</button>
315233
</div>
316-
<pre>{JSON.stringify(table.getState(), null, 2)}</pre>
234+
<pre>
235+
{JSON.stringify(
236+
{ columnFilters: table.getState().columnFilters },
237+
null,
238+
2
239+
)}
240+
</pre>
317241
</div>
318242
)
319243
}
320244

321-
function Filter({
322-
column,
323-
table,
324-
}: {
325-
column: Column<any, unknown>
326-
table: Table<any>
327-
}) {
328-
const firstValue = table
329-
.getPreFilteredRowModel()
330-
.flatRows[0]?.getValue(column.id)
331-
245+
function Filter({ column }: { column: Column<any, unknown> }) {
332246
const columnFilterValue = column.getFilterValue()
247+
const { filterVariant } = column.columnDef.meta ?? {}
333248

334-
const sortedUniqueValues = React.useMemo(
335-
() =>
336-
typeof firstValue === 'number'
337-
? []
338-
: Array.from(column.getFacetedUniqueValues().keys()).sort(),
339-
[column.getFacetedUniqueValues()]
340-
)
341-
342-
return typeof firstValue === 'number' ? (
249+
return filterVariant === 'range' ? (
343250
<div>
344251
<div className="flex space-x-2">
252+
{/* See faceted column filters example for min max values functionality */}
345253
<DebouncedInput
346254
type="number"
347-
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
348-
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
349255
value={(columnFilterValue as [number, number])?.[0] ?? ''}
350256
onChange={value =>
351257
column.setFilterValue((old: [number, number]) => [value, old?.[1]])
352258
}
353-
placeholder={`Min ${
354-
column.getFacetedMinMaxValues()?.[0]
355-
? `(${column.getFacetedMinMaxValues()?.[0]})`
356-
: ''
357-
}`}
259+
placeholder={`Min`}
358260
className="w-24 border shadow rounded"
359261
/>
360262
<DebouncedInput
361263
type="number"
362-
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
363-
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
364264
value={(columnFilterValue as [number, number])?.[1] ?? ''}
365265
onChange={value =>
366266
column.setFilterValue((old: [number, number]) => [old?.[0], value])
367267
}
368-
placeholder={`Max ${
369-
column.getFacetedMinMaxValues()?.[1]
370-
? `(${column.getFacetedMinMaxValues()?.[1]})`
371-
: ''
372-
}`}
268+
placeholder={`Max`}
373269
className="w-24 border shadow rounded"
374270
/>
375271
</div>
376272
<div className="h-1" />
377273
</div>
274+
) : filterVariant === 'select' ? (
275+
<select
276+
onChange={e => column.setFilterValue(e.target.value)}
277+
value={columnFilterValue?.toString()}
278+
>
279+
{/* See faceted column filters example for dynamic select options */}
280+
<option value="">All</option>
281+
<option value="complicated">complicated</option>
282+
<option value="relationship">relationship</option>
283+
<option value="single">single</option>
284+
</select>
378285
) : (
379-
<>
380-
<datalist id={column.id + 'list'}>
381-
{sortedUniqueValues.slice(0, 5000).map((value: any) => (
382-
<option value={value} key={value} />
383-
))}
384-
</datalist>
385-
<DebouncedInput
386-
type="text"
387-
value={(columnFilterValue ?? '') as string}
388-
onChange={value => column.setFilterValue(value)}
389-
placeholder={`Search... (${column.getFacetedUniqueValues().size})`}
390-
className="w-36 border shadow rounded"
391-
list={column.id + 'list'}
392-
/>
393-
<div className="h-1" />
394-
</>
286+
<DebouncedInput
287+
className="w-36 border shadow rounded"
288+
onChange={value => column.setFilterValue(value)}
289+
placeholder={`Search...`}
290+
type="text"
291+
value={(columnFilterValue ?? '') as string}
292+
/>
293+
// See faceted column filters example for datalist search suggestions
395294
)
396295
}
397296

398-
// A debounced input react component
297+
// A typical debounced input react component
399298
function DebouncedInput({
400299
value: initialValue,
401300
onChange,

‎pnpm-lock.yaml

+74
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.