· 6 years ago · Mar 18, 2020, 10:04 AM
1import React from 'react'
2import styled from 'styled-components'
3import { useTable, useExpanded } from 'react-table'
4
5import makeData from './makeData'
6
7const Styles = styled.div`
8 padding: 1rem;
9
10 table {
11 border-spacing: 0;
12 border: 1px solid black;
13
14 tr {
15 :last-child {
16 td {
17 border-bottom: 0;
18 }
19 }
20 }
21
22 th,
23 td {
24 margin: 0;
25 padding: 0.5rem;
26 border-bottom: 1px solid black;
27 border-right: 1px solid black;
28
29 :last-child {
30 border-right: 0;
31 }
32 }
33 }
34`
35
36// A simple way to support a renderRowSubComponent is to make a render prop
37// This is NOT part of the React Table API, it's merely a rendering
38// option we are creating for ourselves in our table renderer
39function Table({ columns: userColumns, data, renderRowSubComponent }) {
40 const {
41 getTableProps,
42 getTableBodyProps,
43 headerGroups,
44 rows,
45 prepareRow,
46 visibleColumns,
47 state: { expanded },
48 } = useTable(
49 {
50 columns: userColumns,
51 data,
52 },
53 useExpanded // We can useExpanded to track the expanded state
54 // for sub components too!
55 )
56
57 return (
58 <>
59 <pre>
60 <code>{JSON.stringify({ expanded: expanded }, null, 2)}</code>
61 </pre>
62 <table {...getTableProps()}>
63 <thead>
64 {headerGroups.map(headerGroup => (
65 <tr {...headerGroup.getHeaderGroupProps()}>
66 {headerGroup.headers.map(column => (
67 <th {...column.getHeaderProps()}>{column.render('Header')}</th>
68 ))}
69 </tr>
70 ))}
71 </thead>
72 <tbody {...getTableBodyProps()}>
73 {rows.map((row, i) => {
74 prepareRow(row)
75 return (
76 // Use a React.Fragment here so the table markup is still valid
77 <React.Fragment {...row.getRowProps()}>
78 <tr>
79 {row.cells.map(cell => {
80 return (
81 <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
82 )
83 })}
84 </tr>
85 {/*
86 If the row is in an expanded state, render a row with a
87 column that fills the entire length of the table.
88 */}
89 {row.isExpanded ? (
90 <tr>
91 <td colSpan={visibleColumns.length}>
92 {/*
93 Inside it, call our renderRowSubComponent function. In reality,
94 you could pass whatever you want as props to
95 a component like this, including the entire
96 table instance. But for this example, we'll just
97 pass the row
98 */}
99 {renderRowSubComponent({ row })}
100 </td>
101 </tr>
102 ) : null}
103 </React.Fragment>
104 )
105 })}
106 </tbody>
107 </table>
108 <br />
109 <div>Showing the first 20 results of {rows.length} rows</div>
110 </>
111 )
112}
113
114function App() {
115 const columns = React.useMemo(
116 () => [
117 {
118 // Make an expander cell
119 Header: () => null, // No header
120 id: 'expander', // It needs an ID
121 Cell: ({ row }) => (
122 // Use Cell to render an expander for each row.
123 // We can use the getToggleRowExpandedProps prop-getter
124 // to build the expander.
125 <span {...row.getToggleRowExpandedProps()}>
126 {row.isExpanded ? '?' : '?'}
127 </span>
128 ),
129 },
130 {
131 Header: 'Name',
132 columns: [
133 {
134 Header: 'First Name',
135 accessor: 'firstName',
136 },
137 {
138 Header: 'Last Name',
139 accessor: 'lastName',
140 },
141 ],
142 },
143 {
144 Header: 'Info',
145 columns: [
146 {
147 Header: 'Age',
148 accessor: 'age',
149 },
150 {
151 Header: 'Visits',
152 accessor: 'visits',
153 },
154 {
155 Header: 'Status',
156 accessor: 'status',
157 },
158 {
159 Header: 'Profile Progress',
160 accessor: 'progress',
161 },
162 ],
163 },
164 ],
165 []
166 )
167
168 const data = React.useMemo(() => makeData(10), [])
169
170 // Create a function that will render our row sub components
171 const renderRowSubComponent = React.useCallback(
172 ({ row }) => (
173 <pre
174 style={{
175 fontSize: '10px',
176 }}
177 >
178 <code>{JSON.stringify({ values: row.values }, null, 2)}</code>
179 </pre>
180 ),
181 []
182 )
183
184 return (
185 <Styles>
186 <Table
187 columns={columns}
188 data={data}
189 // We added this as a prop for our table component
190 // Remember, this is not part of the React Table API,
191 // it's merely a rendering option we created for
192 // ourselves
193 renderRowSubComponent={renderRowSubComponent}
194 />
195 </Styles>
196 )
197}
198
199export default App
200
201
202import React from 'react'
203import styled from 'styled-components'
204import { useTable, useFilters, useGlobalFilter } from 'react-table'
205// A great library for fuzzy filtering/sorting items
206import matchSorter from 'match-sorter'
207
208import makeData from './makeData'
209
210const Styles = styled.div`
211 padding: 1rem;
212
213 table {
214 border-spacing: 0;
215 border: 1px solid black;
216
217 tr {
218 :last-child {
219 td {
220 border-bottom: 0;
221 }
222 }
223 }
224
225 th,
226 td {
227 margin: 0;
228 padding: 0.5rem;
229 border-bottom: 1px solid black;
230 border-right: 1px solid black;
231
232 :last-child {
233 border-right: 0;
234 }
235 }
236 }
237`
238
239// Define a default UI for filtering
240function GlobalFilter({
241 preGlobalFilteredRows,
242 globalFilter,
243 setGlobalFilter,
244}) {
245 const count = preGlobalFilteredRows.length
246
247 return (
248 <span>
249 Search:{' '}
250 <input
251 value={globalFilter || ''}
252 onChange={e => {
253 setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
254 }}
255 placeholder={`${count} records...`}
256 style={{
257 fontSize: '1.1rem',
258 border: '0',
259 }}
260 />
261 </span>
262 )
263}
264
265// Define a default UI for filtering
266function DefaultColumnFilter({
267 column: { filterValue, preFilteredRows, setFilter },
268}) {
269 const count = preFilteredRows.length
270
271 return (
272 <input
273 value={filterValue || ''}
274 onChange={e => {
275 setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
276 }}
277 placeholder={`Search ${count} records...`}
278 />
279 )
280}
281
282// This is a custom filter UI for selecting
283// a unique option from a list
284function SelectColumnFilter({
285 column: { filterValue, setFilter, preFilteredRows, id },
286}) {
287 // Calculate the options for filtering
288 // using the preFilteredRows
289 const options = React.useMemo(() => {
290 const options = new Set()
291 preFilteredRows.forEach(row => {
292 options.add(row.values[id])
293 })
294 return [...options.values()]
295 }, [id, preFilteredRows])
296
297 // Render a multi-select box
298 return (
299 <select
300 value={filterValue}
301 onChange={e => {
302 setFilter(e.target.value || undefined)
303 }}
304 >
305 <option value="">All</option>
306 {options.map((option, i) => (
307 <option key={i} value={option}>
308 {option}
309 </option>
310 ))}
311 </select>
312 )
313}
314
315// This is a custom filter UI that uses a
316// slider to set the filter value between a column's
317// min and max values
318function SliderColumnFilter({
319 column: { filterValue, setFilter, preFilteredRows, id },
320}) {
321 // Calculate the min and max
322 // using the preFilteredRows
323
324 const [min, max] = React.useMemo(() => {
325 let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
326 let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
327 preFilteredRows.forEach(row => {
328 min = Math.min(row.values[id], min)
329 max = Math.max(row.values[id], max)
330 })
331 return [min, max]
332 }, [id, preFilteredRows])
333
334 return (
335 <>
336 <input
337 type="range"
338 min={min}
339 max={max}
340 value={filterValue || min}
341 onChange={e => {
342 setFilter(parseInt(e.target.value, 10))
343 }}
344 />
345 <button onClick={() => setFilter(undefined)}>Off</button>
346 </>
347 )
348}
349
350// This is a custom UI for our 'between' or number range
351// filter. It uses two number boxes and filters rows to
352// ones that have values between the two
353function NumberRangeColumnFilter({
354 column: { filterValue = [], preFilteredRows, setFilter, id },
355}) {
356 const [min, max] = React.useMemo(() => {
357 let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
358 let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
359 preFilteredRows.forEach(row => {
360 min = Math.min(row.values[id], min)
361 max = Math.max(row.values[id], max)
362 })
363 return [min, max]
364 }, [id, preFilteredRows])
365
366 return (
367 <div
368 style={{
369 display: 'flex',
370 }}
371 >
372 <input
373 value={filterValue[0] || ''}
374 type="number"
375 onChange={e => {
376 const val = e.target.value
377 setFilter((old = []) => [val ? parseInt(val, 10) : undefined, old[1]])
378 }}
379 placeholder={`Min (${min})`}
380 style={{
381 width: '70px',
382 marginRight: '0.5rem',
383 }}
384 />
385 to
386 <input
387 value={filterValue[1] || ''}
388 type="number"
389 onChange={e => {
390 const val = e.target.value
391 setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined])
392 }}
393 placeholder={`Max (${max})`}
394 style={{
395 width: '70px',
396 marginLeft: '0.5rem',
397 }}
398 />
399 </div>
400 )
401}
402
403function fuzzyTextFilterFn(rows, id, filterValue) {
404 return matchSorter(rows, filterValue, { keys: [row => row.values[id]] })
405}
406
407// Let the table remove the filter if the string is empty
408fuzzyTextFilterFn.autoRemove = val => !val
409
410// Our table component
411function Table({ columns, data }) {
412 const filterTypes = React.useMemo(
413 () => ({
414 // Add a new fuzzyTextFilterFn filter type.
415 fuzzyText: fuzzyTextFilterFn,
416 // Or, override the default text filter to use
417 // "startWith"
418 text: (rows, id, filterValue) => {
419 return rows.filter(row => {
420 const rowValue = row.values[id]
421 return rowValue !== undefined
422 ? String(rowValue)
423 .toLowerCase()
424 .startsWith(String(filterValue).toLowerCase())
425 : true
426 })
427 },
428 }),
429 []
430 )
431
432 const defaultColumn = React.useMemo(
433 () => ({
434 // Let's set up our default Filter UI
435 Filter: DefaultColumnFilter,
436 }),
437 []
438 )
439
440 const {
441 getTableProps,
442 getTableBodyProps,
443 headerGroups,
444 rows,
445 prepareRow,
446 state,
447 visibleColumns,
448 preGlobalFilteredRows,
449 setGlobalFilter,
450 } = useTable(
451 {
452 columns,
453 data,
454 defaultColumn, // Be sure to pass the defaultColumn option
455 filterTypes,
456 },
457 useFilters, // useFilters!
458 useGlobalFilter // useGlobalFilter!
459 )
460
461 // We don't want to render all of the rows for this example, so cap
462 // it for this use case
463 const firstPageRows = rows.slice(0, 10)
464
465 return (
466 <>
467 <table {...getTableProps()}>
468 <thead>
469 {headerGroups.map(headerGroup => (
470 <tr {...headerGroup.getHeaderGroupProps()}>
471 {headerGroup.headers.map(column => (
472 <th {...column.getHeaderProps()}>
473 {column.render('Header')}
474 {/* Render the columns filter UI */}
475 <div>{column.canFilter ? column.render('Filter') : null}</div>
476 </th>
477 ))}
478 </tr>
479 ))}
480 <tr>
481 <th
482 colSpan={visibleColumns.length}
483 style={{
484 textAlign: 'left',
485 }}
486 >
487 <GlobalFilter
488 preGlobalFilteredRows={preGlobalFilteredRows}
489 globalFilter={state.globalFilter}
490 setGlobalFilter={setGlobalFilter}
491 />
492 </th>
493 </tr>
494 </thead>
495 <tbody {...getTableBodyProps()}>
496 {firstPageRows.map((row, i) => {
497 prepareRow(row)
498 return (
499 <tr {...row.getRowProps()}>
500 {row.cells.map(cell => {
501 return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
502 })}
503 </tr>
504 )
505 })}
506 </tbody>
507 </table>
508 <br />
509 <div>Showing the first 20 results of {rows.length} rows</div>
510 <div>
511 <pre>
512 <code>{JSON.stringify(state.filters, null, 2)}</code>
513 </pre>
514 </div>
515 </>
516 )
517}
518
519// Define a custom filter filter function!
520function filterGreaterThan(rows, id, filterValue) {
521 return rows.filter(row => {
522 const rowValue = row.values[id]
523 return rowValue >= filterValue
524 })
525}
526
527// This is an autoRemove method on the filter function that
528// when given the new filter value and returns true, the filter
529// will be automatically removed. Normally this is just an undefined
530// check, but here, we want to remove the filter if it's not a number
531filterGreaterThan.autoRemove = val => typeof val !== 'number'
532
533function App() {
534 const columns = React.useMemo(
535 () => [
536 {
537 Header: 'Name',
538 columns: [
539 {
540 Header: 'First Name',
541 accessor: 'firstName',
542 },
543 {
544 Header: 'Last Name',
545 accessor: 'lastName',
546 // Use our custom `fuzzyText` filter on this column
547 filter: 'fuzzyText',
548 },
549 ],
550 },
551 {
552 Header: 'Info',
553 columns: [
554 {
555 Header: 'Age',
556 accessor: 'age',
557 Filter: SliderColumnFilter,
558 filter: 'equals',
559 },
560 {
561 Header: 'Visits',
562 accessor: 'visits',
563 Filter: NumberRangeColumnFilter,
564 filter: 'between',
565 },
566 {
567 Header: 'Status',
568 accessor: 'status',
569 Filter: SelectColumnFilter,
570 filter: 'includes',
571 },
572 {
573 Header: 'Profile Progress',
574 accessor: 'progress',
575 Filter: SliderColumnFilter,
576 filter: filterGreaterThan,
577 },
578 ],
579 },
580 ],
581 []
582 )
583
584 const data = React.useMemo(() => makeData(100000), [])
585
586 return (
587 <Styles>
588 <Table columns={columns} data={data} />
589 </Styles>
590 )
591}
592
593export default App