react-table/examples/kitchen-sink-controlled/src/App.js
2019-12-02 01:28:28 -07:00

632 lines
17 KiB
JavaScript

import React from 'react'
import styled from 'styled-components'
import {
useTable,
usePagination,
useSortBy,
useFilters,
useGroupBy,
useExpanded,
useRowSelect,
} from 'react-table'
import matchSorter from 'match-sorter'
import makeData from './makeData'
const Styles = styled.div`
padding: 1rem;
table {
border-spacing: 0;
border: 1px solid black;
tr {
:last-child {
td {
border-bottom: 0;
}
}
}
th,
td {
margin: 0;
padding: 0.5rem;
border-bottom: 1px solid black;
border-right: 1px solid black;
:last-child {
border-right: 0;
}
}
td {
input {
font-size: 1rem;
padding: 0;
margin: 0;
border: 0;
}
}
}
.pagination {
padding: 0.5rem;
}
`
// Create an editable cell renderer
const EditableCell = ({
cell: { value: initialValue },
row: { index },
column: { id },
updateMyData, // This is a custom function that we supplied to our table instance
editable,
}) => {
// We need to keep and update the state of the cell normally
const [value, setValue] = React.useState(initialValue)
const onChange = e => {
setValue(e.target.value)
}
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateMyData(index, id, value)
}
// If the initialValue is changed externall, sync it up with our state
React.useEffect(() => {
setValue(initialValue)
}, [initialValue])
if (!editable) {
return `${initialValue}`
}
return <input value={value} onChange={onChange} onBlur={onBlur} />
}
// Define a default UI for filtering
function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter },
}) {
const count = preFilteredRows.length
return (
<input
value={filterValue || ''}
onChange={e => {
setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder={`Search ${count} records...`}
/>
)
}
// This is a custom filter UI for selecting
// a unique option from a list
function SelectColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id },
}) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set()
preFilteredRows.forEach(row => {
options.add(row.values[id])
})
return [...options.values()]
}, [id, preFilteredRows])
// Render a multi-select box
return (
<select
value={filterValue}
onChange={e => {
setFilter(e.target.value || undefined)
}}
>
<option value="">All</option>
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
)
}
// This is a custom filter UI that uses a
// slider to set the filter value between a column's
// min and max values
function SliderColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id },
}) {
// Calculate the min and max
// using the preFilteredRows
const [min, max] = React.useMemo(() => {
let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
preFilteredRows.forEach(row => {
min = Math.min(row.values[id], min)
max = Math.max(row.values[id], max)
})
return [min, max]
}, [id, preFilteredRows])
return (
<>
<input
type="range"
min={min}
max={max}
value={filterValue || min}
onChange={e => {
setFilter(parseInt(e.target.value, 10))
}}
/>
<button onClick={() => setFilter(undefined)}>Off</button>
</>
)
}
// This is a custom UI for our 'between' or number range
// filter. It uses two number boxes and filters rows to
// ones that have values between the two
function NumberRangeColumnFilter({
column: { filterValue = [], preFilteredRows, setFilter, id },
}) {
const [min, max] = React.useMemo(() => {
let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
preFilteredRows.forEach(row => {
min = Math.min(row.values[id], min)
max = Math.max(row.values[id], max)
})
return [min, max]
}, [id, preFilteredRows])
return (
<div
style={{
display: 'flex',
}}
>
<input
value={filterValue[0] || ''}
type="number"
onChange={e => {
const val = e.target.value
setFilter((old = []) => [val ? parseInt(val, 10) : undefined, old[1]])
}}
placeholder={`Min (${min})`}
style={{
width: '70px',
marginRight: '0.5rem',
}}
/>
to
<input
value={filterValue[1] || ''}
type="number"
onChange={e => {
const val = e.target.value
setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined])
}}
placeholder={`Max (${max})`}
style={{
width: '70px',
marginLeft: '0.5rem',
}}
/>
</div>
)
}
function fuzzyTextFilterFn(rows, id, filterValue) {
return matchSorter(rows, filterValue, { keys: [row => row.values[id]] })
}
// Let the table remove the filter if the string is empty
fuzzyTextFilterFn.autoRemove = val => !val
// Be sure to pass our updateMyData and the skipPageReset option
function Table({ columns, data, updateMyData, skipPageReset }) {
const filterTypes = React.useMemo(
() => ({
// Add a new fuzzyTextFilterFn filter type.
fuzzyText: fuzzyTextFilterFn,
// Or, override the default text filter to use
// "startWith"
text: (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true
})
},
}),
[]
)
const defaultColumn = React.useMemo(
() => ({
// Let's set up our default Filter UI
Filter: DefaultColumnFilter,
// And also our default editable cell
Cell: EditableCell,
}),
[]
)
// Use the state and functions returned from useTable to build your UI
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page, // Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: {
pageIndex,
pageSize,
groupBy,
expanded,
filters,
selectedRowPaths,
},
} = useTable(
{
columns,
data,
defaultColumn,
filterTypes,
// nestExpandedRows: true,
initialState: { pageIndex: 2 },
// updateMyData isn't part of the API, but
// anything we put into these options will
// automatically be available on the instance.
// That way we can call this function from our
// cell renderer!
updateMyData,
// We also need to pass this so the page doesn't change
// when we edit the data, undefined means using the default
getResetPageDeps: skipPageReset ? false : undefined,
},
useFilters,
useSortBy,
useGroupBy,
useExpanded,
usePagination,
useRowSelect
)
// Render the UI for your table
return (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
<div>
{column.canGroupBy ? (
// If the column can be grouped, let's add a toggle
<span {...column.getGroupByToggleProps()}>
{column.isGrouped ? '🛑 ' : '👊 '}
</span>
) : null}
<span {...column.getSortByToggleProps()}>
{column.render('Header')}
{/* Add a sort direction indicator */}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</div>
{/* Render the columns filter UI */}
<div>{column.canFilter ? column.render('Filter') : null}</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>
{cell.isGrouped ? (
// If it's a grouped cell, add an expander and row count
<>
<span {...row.getExpandedToggleProps()}>
{row.isExpanded ? '👇' : '👉'}
</span>{' '}
{cell.render('Cell', { editable: false })} (
{row.subRows.length})
</>
) : cell.isAggregated ? (
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
) : cell.isRepeatedValue ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell', { editable: true })
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
{/*
Pagination can be built however you'd like.
This is just a very basic UI implementation:
*/}
<div className="pagination">
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'<<'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'<'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'>'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
<pre>
<code>
{JSON.stringify(
{
pageIndex,
pageSize,
pageCount,
canNextPage,
canPreviousPage,
groupBy,
expanded,
filters,
selectedRowPaths,
},
null,
2
)}
</code>
</pre>
</>
)
}
// Define a custom filter filter function!
function filterGreaterThan(rows, id, filterValue) {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue >= filterValue
})
}
// This is an autoRemove method on the filter function that
// when given the new filter value and returns true, the filter
// will be automatically removed. Normally this is just an undefined
// check, but here, we want to remove the filter if it's not a number
filterGreaterThan.autoRemove = val => typeof val !== 'number'
// This is a custom aggregator that
// takes in an array of values and
// returns the rounded median
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
values.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
return Math.round((min + max) / 2)
}
function App() {
const columns = React.useMemo(
() => [
{
id: 'selection',
// Make this column a groupByBoundary. This ensures that groupBy columns
// are placed after it
groupByBoundary: true,
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: ({ getToggleAllRowsSelectedProps }) => (
<div>
<input type="checkbox" {...getToggleAllRowsSelectedProps()} />
</div>
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<div>
<input type="checkbox" {...row.getToggleRowSelectedProps()} />
</div>
),
},
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
// Use a two-stage aggregator here to first
// count the total rows being aggregated,
// then sum any of those counts if they are
// aggregated further
aggregate: ['sum', 'count'],
Aggregated: ({ cell: { value } }) => `${value} Names`,
},
{
Header: 'Last Name',
accessor: 'lastName',
// Use our custom `fuzzyText` filter on this column
filter: 'fuzzyText',
// Use another two-stage aggregator here to
// first count the UNIQUE values from the rows
// being aggregated, then sum those counts if
// they are aggregated further
aggregate: ['sum', 'uniqueCount'],
Aggregated: ({ cell: { value } }) => `${value} Unique Names`,
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
Filter: SliderColumnFilter,
filter: 'equals',
// Aggregate the average age of visitors
aggregate: 'average',
Aggregated: ({ cell: { value } }) => `${value} (avg)`,
},
{
Header: 'Visits',
accessor: 'visits',
Filter: NumberRangeColumnFilter,
filter: 'between',
// Aggregate the sum of all visits
aggregate: 'sum',
Aggregated: ({ cell: { value } }) => `${value} (total)`,
},
{
Header: 'Status',
accessor: 'status',
Filter: SelectColumnFilter,
filter: 'includes',
},
{
Header: 'Profile Progress',
accessor: 'progress',
Filter: SliderColumnFilter,
filter: filterGreaterThan,
// Use our custom roundedMedian aggregator
aggregate: roundedMedian,
Aggregated: ({ cell: { value } }) => `${value} (med)`,
},
],
},
],
[]
)
const [data, setData] = React.useState(() => makeData(10000))
const [originalData] = React.useState(data)
// We need to keep the table from resetting the pageIndex when we
// Update data. So we can keep track of that flag with a ref.
const skipPageResetRef = React.useRef(false)
// When our cell renderer calls updateMyData, we'll use
// the rowIndex, columnId and new value to update the
// original data
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
skipPageResetRef.current = true
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...row,
[columnId]: value,
}
}
return row
})
)
}
// After data chagnes, we turn the flag back off
// so that if data actually changes when we're not
// editing it, the page is reset
React.useEffect(() => {
skipPageResetRef.current = false
}, [data])
// Let's add a data resetter/randomizer to help
// illustrate that flow...
const resetData = () => {
// Don't reset the page when we do this
skipPageResetRef.current = true
setData(originalData)
}
return (
<Styles>
<button onClick={resetData}>Reset Data</button>
<Table
columns={columns}
data={data}
updateMyData={updateMyData}
skipPageReset={skipPageResetRef.current}
/>
</Styles>
)
}
export default App