mirror of
https://github.com/gosticks/react-table.git
synced 2026-06-29 01:20:02 +00:00
Refactor useTable, sorting, and filtering to use new hook layer
This commit is contained in:
18
README.md
18
README.md
@@ -33,9 +33,7 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
|
||||
- Extensible via hooks
|
||||
- <a href="https://medium.com/@tannerlinsley/why-i-wrote-react-table-and-the-problems-it-has-solved-for-nozzle-others-445c4e93d4a8#.axza4ixba" target="\_parent">"Why I wrote React Table and the problems it has solved for Nozzle.io"</a> by Tanner Linsley
|
||||
|
||||
## Demos
|
||||
|
||||
[React Table v7 Sandbox](https://codesandbox.io/s/m5lxzzpz69)
|
||||
## [See Examples](#examples)
|
||||
|
||||
## Versions
|
||||
|
||||
@@ -237,12 +235,15 @@ const instance = useTable(
|
||||
|
||||
1. `useTable` is called. A table instance is created.
|
||||
1. The `instance.state` is resolved from either a custom user state or an automatically generated one.
|
||||
1. A collection of plugin points is created at `instance.hooks`. These plugin points don't run until after all of the plugins have run.
|
||||
1. The instance is reduced through each plugin hook in the order they were called. Each hook receives the result of the previous hook, is able to manipulate the `instance`, use plugin points, use their own React hooks internally and eventually return a new `instance`. This happens until the last instance object is returned from the last hook.
|
||||
1. Lastly, the plugin points that were registered and populated during hook reduction are run to produce the final instance object that is returned from `useTable`
|
||||
1. A collection of plugin points is created at `instance.hooks`.
|
||||
1. Each plugin is given the opportunity to add hooks to `instance.hook`.
|
||||
1. As the `useTable` logic proceeds to run, each plugin hook type is used at a specific point in time with each individual hook function being executed the order it was registered.
|
||||
1. The final instance object is returned from `useTable`, which the developer then uses to construct their table.
|
||||
|
||||
This multi-stage process is the secret sauce that allows React Table plugin hooks to work together and compose nicely, while not stepping on each others toes.
|
||||
|
||||
To dive deeper into plugins, see [Plugins](TODO) and the [Plugin Guide](TODO)
|
||||
|
||||
### Plugin Hook Order & Consistency
|
||||
|
||||
The order and usage of plugin hooks must follow [The Laws of Hooks](TODO), just like any other custom hook. They must always be unconditionally called in the same order.
|
||||
@@ -286,9 +287,6 @@ The following options are supported via the main options object passed to `useTa
|
||||
- The default column object for every column passed to React Table.
|
||||
- Column-specific properties will override the properties in this object, eg. `{ ...defaultColumn, ...userColumn }`
|
||||
- This is particularly useful for adding global column properties. For instance, when using the `useFilters` plugin hook, add a default `Filter` renderer for every column, eg.`{ Filter: MyDefaultFilterComponent }`
|
||||
- `useRows: Function`
|
||||
- Optional
|
||||
- This hook overrides the internal `useRows` hooks used by `useTable`. You probably never want to override this unless you are testing or developing new features for React Table
|
||||
- `debug: Bool`
|
||||
- Optional
|
||||
- A flag to turn on debug mode.
|
||||
@@ -580,7 +578,7 @@ The following values are provided to the table `instance`:
|
||||
|
||||
The following properties are available on every `Column` object returned by the table instance.
|
||||
|
||||
- `canSortBy: Bool`
|
||||
- `canSort: Bool`
|
||||
- Denotes whether a column is sortable or not depending on if it has a valid accessor/data model or is manually disabled via an option.
|
||||
- `toggleSortBy: Function(descending, multi) => void`
|
||||
- This function can be used to programmatically toggle the sorting for this column.
|
||||
|
||||
@@ -33,47 +33,72 @@ const Styles = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const defaultColumn = {
|
||||
sort: 'numeric',
|
||||
}
|
||||
|
||||
function Table({ columns, data }) {
|
||||
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
defaultColumn,
|
||||
debug: true,
|
||||
},
|
||||
useSortBy
|
||||
)
|
||||
|
||||
// We don't want to render all 2000 rows for this example, so cap
|
||||
// it at 20 for this use case
|
||||
const firstPageRows = rows.slice(0, 20)
|
||||
|
||||
return (
|
||||
<table {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
|
||||
{column.render('Header')}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<>
|
||||
<table {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(
|
||||
column =>
|
||||
console.log(column) || (
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th
|
||||
{...column.getHeaderProps(column.getSortByToggleProps())}
|
||||
>
|
||||
{column.render('Header')}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.sorted
|
||||
? column.sortedDesc
|
||||
? ' 🔽'
|
||||
: ' 🔼'
|
||||
: ''}
|
||||
</span>
|
||||
</th>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{firstPageRows.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<div>Showing the first 20 results of {rows.length} rows</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,7 +143,7 @@ function App() {
|
||||
[]
|
||||
)
|
||||
|
||||
const data = React.useMemo(() => makeData(20), [])
|
||||
const data = React.useMemo(() => makeData(1000), [])
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
|
||||
@@ -89,6 +89,4 @@ export const between = (rows, id, filterValue) => {
|
||||
}
|
||||
|
||||
between.autoRemove = val =>
|
||||
console.log(val) ||
|
||||
!val ||
|
||||
(typeof val[0] !== 'number' && typeof val[1] !== 'number')
|
||||
!val || (typeof val[0] !== 'number' && typeof val[1] !== 'number')
|
||||
|
||||
111
src/hooks/tests/useTable.test.js
Normal file
111
src/hooks/tests/useTable.test.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import '@testing-library/react/cleanup-after-each'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
|
||||
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import useTable from './useTable'
|
||||
|
||||
function Table({ columns, data }) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const { getTableProps, headerGroups, rows, prepareRow } = useTable({
|
||||
columns,
|
||||
data,
|
||||
})
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<table {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
columns: [
|
||||
{
|
||||
Header: 'First Name',
|
||||
accessor: 'firstName',
|
||||
},
|
||||
{
|
||||
Header: 'Last Name',
|
||||
accessor: 'lastName',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: 'Info',
|
||||
columns: [
|
||||
{
|
||||
Header: 'Age',
|
||||
accessor: 'age',
|
||||
},
|
||||
{
|
||||
Header: 'Visits',
|
||||
accessor: 'visits',
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
accessor: 'status',
|
||||
},
|
||||
{
|
||||
Header: 'Profile Progress',
|
||||
accessor: 'progress',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const data = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
firstName: 'tanner',
|
||||
lastName: 'linsley',
|
||||
age: 29,
|
||||
visits: 100,
|
||||
status: 'In Relationship',
|
||||
progress: 50,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
return <Table columns={columns} data={data} />
|
||||
}
|
||||
|
||||
test('renders a basic table', () => {
|
||||
const { getByText } = render(<App />)
|
||||
|
||||
expect(getByText('tanner')).toBeInTheDocument()
|
||||
expect(getByText('linsley')).toBeInTheDocument()
|
||||
expect(getByText('29')).toBeInTheDocument()
|
||||
expect(getByText('100')).toBeInTheDocument()
|
||||
expect(getByText('In Relationship')).toBeInTheDocument()
|
||||
expect(getByText('50')).toBeInTheDocument()
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
decorateColumnTree,
|
||||
makeHeaderGroups,
|
||||
findMaxDepth,
|
||||
flattenBy,
|
||||
} from '../utils'
|
||||
|
||||
import { useTableState } from './useTableState'
|
||||
@@ -52,34 +53,36 @@ export const useTable = (props, ...plugins) => {
|
||||
// But use the users state if provided
|
||||
const state = userState || defaultState
|
||||
|
||||
// These are hooks that plugins can use right before render
|
||||
const hooks = {
|
||||
beforeRender: [],
|
||||
columns: [],
|
||||
headers: [],
|
||||
headerGroups: [],
|
||||
rows: [],
|
||||
row: [],
|
||||
getTableProps: [],
|
||||
getRowProps: [],
|
||||
getHeaderGroupProps: [],
|
||||
getHeaderProps: [],
|
||||
getCellProps: [],
|
||||
}
|
||||
|
||||
// The initial api
|
||||
let api = {
|
||||
let instanceRef = React.useRef({})
|
||||
|
||||
Object.assign(instanceRef.current, {
|
||||
...props,
|
||||
data,
|
||||
state,
|
||||
hooks,
|
||||
plugins,
|
||||
}
|
||||
hooks: {
|
||||
columnsBeforeHeaderGroups: [],
|
||||
useMain: [],
|
||||
useColumns: [],
|
||||
useHeaders: [],
|
||||
useHeaderGroups: [],
|
||||
useRows: [],
|
||||
prepareRow: [],
|
||||
getTableProps: [],
|
||||
getRowProps: [],
|
||||
getHeaderGroupProps: [],
|
||||
getHeaderProps: [],
|
||||
getCellProps: [],
|
||||
},
|
||||
})
|
||||
|
||||
if (debug) console.time('hooks')
|
||||
// Loop through plugins to build the api out
|
||||
api = plugins.filter(Boolean).reduce((prev, next) => next(prev), api)
|
||||
if (debug) console.timeEnd('hooks')
|
||||
// Allow plugins to register hooks
|
||||
if (debug) console.time('plugins')
|
||||
plugins.filter(Boolean).forEach(plugin => {
|
||||
plugin(instanceRef.current.hooks)
|
||||
})
|
||||
if (debug) console.timeEnd('plugins')
|
||||
|
||||
// Compute columns, headerGroups and headers
|
||||
const columnInfo = React.useMemo(
|
||||
@@ -93,7 +96,11 @@ export const useTable = (props, ...plugins) => {
|
||||
|
||||
// Allow hooks to decorate columns
|
||||
if (debug) console.time('hooks.columnsBeforeHeaderGroups')
|
||||
columns = applyHooks(api.hooks.columnsBeforeHeaderGroups, columns, api)
|
||||
columns = applyHooks(
|
||||
instanceRef.current.hooks.columnsBeforeHeaderGroups,
|
||||
columns,
|
||||
instanceRef.current
|
||||
)
|
||||
if (debug) console.timeEnd('hooks.columnsBeforeHeaderGroups')
|
||||
|
||||
// Make the headerGroups
|
||||
@@ -111,17 +118,16 @@ export const useTable = (props, ...plugins) => {
|
||||
headers,
|
||||
}
|
||||
},
|
||||
[api, debug, defaultColumn, userColumns]
|
||||
[debug, defaultColumn, userColumns]
|
||||
)
|
||||
|
||||
// Place the columns, headerGroups and headers on the api
|
||||
Object.assign(api, columnInfo)
|
||||
Object.assign(instanceRef.current, columnInfo)
|
||||
|
||||
// Access the row model
|
||||
api.rows = React.useMemo(
|
||||
instanceRef.current.rows = React.useMemo(
|
||||
() => {
|
||||
if (debug) console.info('getAccessedRows')
|
||||
|
||||
if (debug) console.time('getAccessedRows')
|
||||
// Access the row's data
|
||||
const accessRow = (originalRow, i, depth = 0) => {
|
||||
// Keep the original reference around
|
||||
@@ -155,7 +161,7 @@ export const useTable = (props, ...plugins) => {
|
||||
|
||||
// Create the cells and values
|
||||
row.values = {}
|
||||
api.columns.forEach(column => {
|
||||
instanceRef.current.columns.forEach(column => {
|
||||
row.values[column.id] = column.accessor
|
||||
? column.accessor(originalRow, i, { subRows, depth, data })
|
||||
: undefined
|
||||
@@ -165,61 +171,89 @@ export const useTable = (props, ...plugins) => {
|
||||
}
|
||||
|
||||
// Use the resolved data
|
||||
return data.map((d, i) => accessRow(d, i))
|
||||
const accessedData = data.map((d, i) => accessRow(d, i))
|
||||
if (debug) console.timeEnd('getAccessedRows')
|
||||
return accessedData
|
||||
},
|
||||
[debug, data, subRowsKey, api.columns]
|
||||
[debug, data, subRowsKey]
|
||||
)
|
||||
|
||||
// Determine column visibility
|
||||
api.columns.forEach(column => {
|
||||
instanceRef.current.columns.forEach(column => {
|
||||
column.visible =
|
||||
typeof column.show === 'function' ? column.show(api) : !!column.show
|
||||
typeof column.show === 'function'
|
||||
? column.show(instanceRef.current)
|
||||
: !!column.show
|
||||
})
|
||||
|
||||
// Allow hooks to decorate columns
|
||||
if (debug) console.time('hooks.columns')
|
||||
api.columns = applyHooks(api.hooks.columns, api.columns, api)
|
||||
if (debug) console.timeEnd('hooks.columns')
|
||||
if (debug) console.time('hooks.useMain')
|
||||
instanceRef.current = applyHooks(
|
||||
instanceRef.current.hooks.useMain,
|
||||
instanceRef.current
|
||||
)
|
||||
if (debug)
|
||||
console.timeEnd('hooks.useMain')
|
||||
|
||||
// Allow hooks to decorate headers
|
||||
if (debug) console.time('hooks.headers')
|
||||
api.headers = applyHooks(api.hooks.headers, api.headers, api)
|
||||
if (debug) console.timeEnd('hooks.headers')
|
||||
;[...api.columns, ...api.headers].forEach(column => {
|
||||
// Give columns/headers rendering power
|
||||
column.render = (type, userProps = {}) => {
|
||||
const Comp = typeof type === 'string' ? column[type] : type
|
||||
// // Allow hooks to decorate columns
|
||||
// if (debug) console.time('hooks.useColumns')
|
||||
// instanceRef.current.columns = applyHooks(
|
||||
// instanceRef.current.hooks.useColumns,
|
||||
// instanceRef.current.columns,
|
||||
// instanceRef.current
|
||||
// )
|
||||
// if (debug) console.timeEnd('hooks.useColumns')
|
||||
|
||||
if (typeof Comp === 'undefined') {
|
||||
throw new Error(renderErr)
|
||||
// // Allow hooks to decorate headers
|
||||
// if (debug) console.time('hooks.useHeaders')
|
||||
// instanceRef.current.headers = applyHooks(
|
||||
// instanceRef.current.hooks.useHeaders,
|
||||
// instanceRef.current.headers,
|
||||
// instanceRef.current
|
||||
// )
|
||||
// if (debug) console.timeEnd('hooks.useHeaders')
|
||||
;[...instanceRef.current.columns, ...instanceRef.current.headers].forEach(
|
||||
column => {
|
||||
// Give columns/headers rendering power
|
||||
column.render = (type, userProps = {}) => {
|
||||
const Comp = typeof type === 'string' ? column[type] : type
|
||||
|
||||
if (typeof Comp === 'undefined') {
|
||||
throw new Error(renderErr)
|
||||
}
|
||||
|
||||
return flexRender(Comp, {
|
||||
...instanceRef.current,
|
||||
...column,
|
||||
...userProps,
|
||||
})
|
||||
}
|
||||
|
||||
return flexRender(Comp, {
|
||||
...api,
|
||||
...column,
|
||||
...userProps,
|
||||
})
|
||||
// Give columns/headers a default getHeaderProps
|
||||
column.getHeaderProps = props =>
|
||||
mergeProps(
|
||||
{
|
||||
key: ['header', column.id].join('_'),
|
||||
colSpan: column.columns ? column.columns.length : 1,
|
||||
},
|
||||
applyPropHooks(
|
||||
instanceRef.current.hooks.getHeaderProps,
|
||||
column,
|
||||
instanceRef.current
|
||||
),
|
||||
props
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Give columns/headers a default getHeaderProps
|
||||
column.getHeaderProps = props =>
|
||||
mergeProps(
|
||||
{
|
||||
key: ['header', column.id].join('_'),
|
||||
colSpan: column.columns ? column.columns.length : 1,
|
||||
},
|
||||
applyPropHooks(api.hooks.getHeaderProps, column, api),
|
||||
props
|
||||
)
|
||||
})
|
||||
// // Allow hooks to decorate headerGroups
|
||||
// if (debug) console.time('hooks.useHeaderGroups')
|
||||
// instanceRef.current.headerGroups = applyHooks(
|
||||
// instanceRef.current.hooks.useHeaderGroups,
|
||||
// instanceRef.current.headerGroups,
|
||||
// instanceRef.current
|
||||
// )
|
||||
|
||||
// Allow hooks to decorate headerGroups
|
||||
if (debug) console.time('hooks.headerGroups')
|
||||
api.headerGroups = applyHooks(
|
||||
api.hooks.headerGroups,
|
||||
api.headerGroups,
|
||||
api
|
||||
).filter((headerGroup, i) => {
|
||||
instanceRef.current.headerGroups.filter((headerGroup, i) => {
|
||||
// Filter out any headers and headerGroups that don't have visible columns
|
||||
headerGroup.headers = headerGroup.headers.filter(header => {
|
||||
const recurse = columns =>
|
||||
@@ -242,7 +276,11 @@ export const useTable = (props, ...plugins) => {
|
||||
{
|
||||
key: [`header${i}`].join('_'),
|
||||
},
|
||||
applyPropHooks(api.hooks.getHeaderGroupProps, headerGroup, api),
|
||||
applyPropHooks(
|
||||
instanceRef.current.hooks.getHeaderGroupProps,
|
||||
headerGroup,
|
||||
instanceRef.current
|
||||
),
|
||||
props
|
||||
)
|
||||
return true
|
||||
@@ -250,29 +288,39 @@ export const useTable = (props, ...plugins) => {
|
||||
|
||||
return false
|
||||
})
|
||||
if (debug) console.timeEnd('hooks.headerGroups')
|
||||
// if (debug) console.timeEnd('hooks.useHeaderGroups')
|
||||
|
||||
// Run the rows (this could be a dangerous hook with a ton of data)
|
||||
if (debug) console.time('hooks.rows')
|
||||
api.rows = applyHooks(api.hooks.rows, api.rows, api)
|
||||
if (debug) console.timeEnd('hooks.rows')
|
||||
if (debug) console.time('hooks.useRows')
|
||||
instanceRef.current.rows = applyHooks(
|
||||
instanceRef.current.hooks.useRows,
|
||||
instanceRef.current.rows,
|
||||
instanceRef.current
|
||||
)
|
||||
if (debug) console.timeEnd('hooks.useRows')
|
||||
|
||||
// The prepareRow function is absolutely necessary and MUST be called on
|
||||
// any rows the user wishes to be displayed.
|
||||
|
||||
api.prepareRow = row => {
|
||||
instanceRef.current.prepareRow = row => {
|
||||
const { path } = row
|
||||
row.getRowProps = props =>
|
||||
mergeProps(
|
||||
{ key: ['row', ...path].join('_') },
|
||||
applyPropHooks(api.hooks.getRowProps, row, api),
|
||||
applyPropHooks(
|
||||
instanceRef.current.hooks.getRowProps,
|
||||
row,
|
||||
instanceRef.current
|
||||
),
|
||||
props
|
||||
)
|
||||
|
||||
// need to apply any row specific hooks (useExpanded requires this)
|
||||
applyHooks(api.hooks.row, row, api)
|
||||
applyHooks(instanceRef.current.hooks.prepareRow, row, instanceRef.current)
|
||||
|
||||
const visibleColumns = api.columns.filter(column => column.visible)
|
||||
const visibleColumns = instanceRef.current.columns.filter(
|
||||
column => column.visible
|
||||
)
|
||||
|
||||
// Build the cells for each row
|
||||
row.cells = visibleColumns.map(column => {
|
||||
@@ -289,7 +337,11 @@ export const useTable = (props, ...plugins) => {
|
||||
{
|
||||
key: ['cell', columnPathStr].join('_'),
|
||||
},
|
||||
applyPropHooks(api.hooks.getCellProps, cell, api),
|
||||
applyPropHooks(
|
||||
instanceRef.current.hooks.getCellProps,
|
||||
cell,
|
||||
instanceRef.current
|
||||
),
|
||||
props
|
||||
)
|
||||
}
|
||||
@@ -303,7 +355,7 @@ export const useTable = (props, ...plugins) => {
|
||||
}
|
||||
|
||||
return flexRender(Comp, {
|
||||
...api,
|
||||
...instanceRef.current,
|
||||
...cell,
|
||||
...userProps,
|
||||
})
|
||||
@@ -313,26 +365,14 @@ export const useTable = (props, ...plugins) => {
|
||||
})
|
||||
}
|
||||
|
||||
api.getTableProps = userProps =>
|
||||
mergeProps(applyPropHooks(api.hooks.getTableProps, api), userProps)
|
||||
instanceRef.current.getTableProps = userProps =>
|
||||
mergeProps(
|
||||
applyPropHooks(
|
||||
instanceRef.current.hooks.getTableProps,
|
||||
instanceRef.current
|
||||
),
|
||||
userProps
|
||||
)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
function flattenBy(columns, childKey) {
|
||||
const flatColumns = []
|
||||
|
||||
const recurse = columns => {
|
||||
columns.forEach(d => {
|
||||
if (!d[childKey]) {
|
||||
flatColumns.push(d)
|
||||
} else {
|
||||
recurse(d[childKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
recurse(columns)
|
||||
|
||||
return flatColumns
|
||||
return instanceRef.current
|
||||
}
|
||||
|
||||
@@ -38,45 +38,48 @@ export const useExpanded = props => {
|
||||
}, actions.toggleExpanded)
|
||||
}
|
||||
|
||||
hooks.row.push(row => {
|
||||
hooks.prepareRow.push(row => {
|
||||
const { path } = row
|
||||
row.toggleExpanded = set => toggleExpandedByPath(path, set)
|
||||
return row
|
||||
})
|
||||
|
||||
const expandedRows = useMemo(() => {
|
||||
if (debug) console.info('getExpandedRows')
|
||||
const expandedRows = useMemo(
|
||||
() => {
|
||||
if (debug) console.info('getExpandedRows')
|
||||
|
||||
const expandedRows = []
|
||||
const expandedRows = []
|
||||
|
||||
// Here we do some mutation, but it's the last stage in the
|
||||
// immutable process so this is safe
|
||||
const handleRow = (row, depth = 0, parentPath = []) => {
|
||||
// Compute some final state for the row
|
||||
const path = [...parentPath, row.index]
|
||||
// Here we do some mutation, but it's the last stage in the
|
||||
// immutable process so this is safe
|
||||
const handleRow = (row, depth = 0, parentPath = []) => {
|
||||
// Compute some final state for the row
|
||||
const path = [...parentPath, row.index]
|
||||
|
||||
row.path = path
|
||||
row.depth = depth
|
||||
row.path = path
|
||||
row.depth = depth
|
||||
|
||||
row.isExpanded =
|
||||
(row.original && row.original[manualExpandedKey]) ||
|
||||
getBy(expanded, path)
|
||||
row.isExpanded =
|
||||
(row.original && row.original[manualExpandedKey]) ||
|
||||
getBy(expanded, path)
|
||||
|
||||
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
|
||||
expandedRows.push(row)
|
||||
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
|
||||
expandedRows.push(row)
|
||||
}
|
||||
|
||||
if (row.isExpanded && row.subRows && row.subRows.length) {
|
||||
row.subRows.forEach((row, i) => handleRow(row, depth + 1, path))
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
if (row.isExpanded && row.subRows && row.subRows.length) {
|
||||
row.subRows.forEach((row, i) => handleRow(row, depth + 1, path))
|
||||
}
|
||||
rows.forEach(row => handleRow(row))
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
rows.forEach(row => handleRow(row))
|
||||
|
||||
return expandedRows
|
||||
}, [debug, rows, manualExpandedKey, expanded, paginateSubRows])
|
||||
return expandedRows
|
||||
},
|
||||
[debug, rows, manualExpandedKey, expanded, paginateSubRows]
|
||||
)
|
||||
|
||||
const expandedDepth = findExpandedDepth(expanded)
|
||||
|
||||
|
||||
@@ -22,8 +22,12 @@ const propTypes = {
|
||||
manualFilters: PropTypes.bool,
|
||||
}
|
||||
|
||||
export const useFilters = props => {
|
||||
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFilters')
|
||||
export const useFilters = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useFilters')
|
||||
|
||||
const {
|
||||
debug,
|
||||
@@ -32,9 +36,8 @@ export const useFilters = props => {
|
||||
filterTypes: userFilterTypes,
|
||||
manualFilters,
|
||||
disableFilters,
|
||||
hooks,
|
||||
state: [{ filters }, setState],
|
||||
} = props
|
||||
} = instance
|
||||
|
||||
const setFilter = (id, updater) => {
|
||||
const column = columns.find(d => d.id === id)
|
||||
@@ -94,28 +97,24 @@ export const useFilters = props => {
|
||||
}, actions.setAllFilters)
|
||||
}
|
||||
|
||||
hooks.columns.push(columns => {
|
||||
columns.forEach(column => {
|
||||
const { id, accessor, disableFilters: columnDisableFilters } = column
|
||||
columns.forEach(column => {
|
||||
const { id, accessor, disableFilters: columnDisableFilters } = column
|
||||
|
||||
// Determine if a column is filterable
|
||||
column.canFilter = accessor
|
||||
? getFirstDefined(
|
||||
columnDisableFilters,
|
||||
disableFilters === true ? false : undefined,
|
||||
true
|
||||
)
|
||||
: false
|
||||
// Determine if a column is filterable
|
||||
column.canFilter = accessor
|
||||
? getFirstDefined(
|
||||
columnDisableFilters,
|
||||
disableFilters === true ? false : undefined,
|
||||
true
|
||||
)
|
||||
: false
|
||||
|
||||
// Provide the column a way of updating the filter value
|
||||
column.setFilter = val => setFilter(column.id, val)
|
||||
// Provide the column a way of updating the filter value
|
||||
column.setFilter = val => setFilter(column.id, val)
|
||||
|
||||
// Provide the current filter value to the column for
|
||||
// convenience
|
||||
column.filterValue = filters[id]
|
||||
})
|
||||
|
||||
return columns
|
||||
// Provide the current filter value to the column for
|
||||
// convenience
|
||||
column.filterValue = filters[id]
|
||||
})
|
||||
|
||||
// TODO: Create a filter cache for incremental high speed multi-filtering
|
||||
@@ -199,7 +198,7 @@ export const useFilters = props => {
|
||||
)
|
||||
|
||||
return {
|
||||
...props,
|
||||
...instance,
|
||||
setFilter,
|
||||
setAllFilters,
|
||||
preFilteredRows: rows,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const useGroupBy = props => {
|
||||
|
||||
// Sort grouped columns to the start of the column list
|
||||
// before the headers are built
|
||||
hooks.columnsBeforeHeaderGroups.push(columns => {
|
||||
hooks.useColumnsBeforeHeaderGroups.push(columns => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return React.useMemo(
|
||||
() => [
|
||||
@@ -92,7 +92,7 @@ export const useGroupBy = props => {
|
||||
}, actions.toggleGroupBy)
|
||||
}
|
||||
|
||||
hooks.columns.push(columns => {
|
||||
hooks.useColumns.push(columns => {
|
||||
columns.forEach(column => {
|
||||
if (column.canGroupBy) {
|
||||
column.toggleGroupBy = () => toggleGroupBy(column.id)
|
||||
@@ -128,8 +128,8 @@ export const useGroupBy = props => {
|
||||
return columns
|
||||
}
|
||||
|
||||
hooks.columns.push(addGroupByToggleProps)
|
||||
hooks.headers.push(addGroupByToggleProps)
|
||||
hooks.useColumns.push(addGroupByToggleProps)
|
||||
hooks.useHeaders.push(addGroupByToggleProps)
|
||||
|
||||
const groupedRows = useMemo(
|
||||
() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { addActions, actions } from '../actions'
|
||||
@@ -34,8 +34,12 @@ const propTypes = {
|
||||
disableMultiRemove: PropTypes.bool,
|
||||
}
|
||||
|
||||
export const useSortBy = props => {
|
||||
PropTypes.checkPropTypes(propTypes, props, 'property', 'useSortBy')
|
||||
export const useSortBy = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useSortBy')
|
||||
|
||||
const {
|
||||
debug,
|
||||
@@ -51,7 +55,7 @@ export const useSortBy = props => {
|
||||
hooks,
|
||||
state: [{ sortBy }, setState],
|
||||
plugins,
|
||||
} = props
|
||||
} = instance
|
||||
|
||||
// If useSortBy should probably come after useFilters for
|
||||
// the best performance, so let's hint to the user about that...
|
||||
@@ -67,16 +71,8 @@ export const useSortBy = props => {
|
||||
)
|
||||
}
|
||||
|
||||
columns.forEach(column => {
|
||||
const { accessor, disableSorting: columnDisableSorting } = column
|
||||
column.canSortBy = accessor
|
||||
? getFirstDefined(
|
||||
columnDisableSorting,
|
||||
disableSorting === true ? false : undefined,
|
||||
true
|
||||
)
|
||||
: false
|
||||
})
|
||||
// Add custom hooks
|
||||
hooks.getSortByToggleProps = []
|
||||
|
||||
// Updates sorting based on a columnID, desc flag and multi flag
|
||||
const toggleSortBy = (columnID, desc, multi) => {
|
||||
@@ -164,63 +160,58 @@ export const useSortBy = props => {
|
||||
}, actions.sortByChange)
|
||||
}
|
||||
|
||||
hooks.columns.push(columns => {
|
||||
columns.forEach(column => {
|
||||
if (column.canSortBy) {
|
||||
column.toggleSortBy = (desc, multi) =>
|
||||
toggleSortBy(column.id, desc, multi)
|
||||
}
|
||||
})
|
||||
return columns
|
||||
})
|
||||
// Add the getSortByToggleProps method to columns and headers
|
||||
;[...instance.columns, ...instance.headers].forEach(column => {
|
||||
const { accessor, disableSorting: columnDisableSorting, id } = column
|
||||
|
||||
hooks.getSortByToggleProps = []
|
||||
|
||||
const addSortByToggleProps = (columns, api) => {
|
||||
columns.forEach(column => {
|
||||
const { canSortBy } = column
|
||||
column.getSortByToggleProps = props => {
|
||||
return mergeProps(
|
||||
{
|
||||
onClick: canSortBy
|
||||
? e => {
|
||||
e.persist()
|
||||
column.toggleSortBy(
|
||||
undefined,
|
||||
!api.disableMultiSort && e.shiftKey
|
||||
)
|
||||
}
|
||||
: undefined,
|
||||
style: {
|
||||
cursor: canSortBy ? 'pointer' : undefined,
|
||||
},
|
||||
title: 'Toggle SortBy',
|
||||
},
|
||||
applyPropHooks(api.hooks.getSortByToggleProps, column, api),
|
||||
props
|
||||
const canSort = accessor
|
||||
? getFirstDefined(
|
||||
columnDisableSorting,
|
||||
disableSorting === true ? false : undefined,
|
||||
true
|
||||
)
|
||||
}
|
||||
})
|
||||
return columns
|
||||
}
|
||||
: false
|
||||
|
||||
hooks.columns.push(addSortByToggleProps)
|
||||
hooks.headers.push(addSortByToggleProps)
|
||||
column.canSort = canSort
|
||||
|
||||
if (column.canSort) {
|
||||
column.toggleSortBy = (desc, multi) =>
|
||||
toggleSortBy(column.id, desc, multi)
|
||||
}
|
||||
|
||||
column.getSortByToggleProps = props => {
|
||||
return mergeProps(
|
||||
{
|
||||
onClick: canSort
|
||||
? e => {
|
||||
e.persist()
|
||||
column.toggleSortBy(
|
||||
undefined,
|
||||
!instance.disableMultiSort && e.shiftKey
|
||||
)
|
||||
}
|
||||
: undefined,
|
||||
style: {
|
||||
cursor: canSort ? 'pointer' : undefined,
|
||||
},
|
||||
title: 'Toggle SortBy',
|
||||
},
|
||||
applyPropHooks(instance.hooks.getSortByToggleProps, column, instance),
|
||||
props
|
||||
)
|
||||
}
|
||||
|
||||
// Mutate columns to reflect sorting state
|
||||
columns.forEach(column => {
|
||||
const { id } = column
|
||||
column.sorted = sortBy.find(d => d.id === id)
|
||||
column.sortedIndex = sortBy.findIndex(d => d.id === id)
|
||||
column.sortedDesc = column.sorted ? column.sorted.desc : undefined
|
||||
})
|
||||
|
||||
const sortedRows = useMemo(
|
||||
const sortedRows = React.useMemo(
|
||||
() => {
|
||||
if (manualSorting || !sortBy.length) {
|
||||
return rows
|
||||
}
|
||||
if (debug) console.info('getSortedRows')
|
||||
if (debug) console.time('getSortedRows')
|
||||
|
||||
const sortData = rows => {
|
||||
// Use the orderByFn to compose multiple sortBy's together.
|
||||
@@ -270,6 +261,8 @@ export const useSortBy = props => {
|
||||
row.subRows = sortData(row.subRows)
|
||||
})
|
||||
|
||||
if (debug) console.timeEnd('getSortedRows')
|
||||
|
||||
return sortedData
|
||||
}
|
||||
|
||||
@@ -279,7 +272,7 @@ export const useSortBy = props => {
|
||||
)
|
||||
|
||||
return {
|
||||
...props,
|
||||
...instance,
|
||||
toggleSortBy,
|
||||
rows: sortedRows,
|
||||
preSortedRows: rows,
|
||||
|
||||
28
src/utils.js
28
src/utils.js
@@ -260,7 +260,15 @@ export const mergeProps = (...groups) => {
|
||||
}
|
||||
|
||||
export const applyHooks = (hooks, initial, ...args) =>
|
||||
hooks.reduce((prev, next) => next(prev, ...args), initial)
|
||||
hooks.reduce((prev, next) => {
|
||||
const nextValue = next(prev, ...args)
|
||||
if (typeof nextValue === 'undefined') {
|
||||
throw new Error(
|
||||
'React Table: A hook just returned undefined! This is not allowed.'
|
||||
)
|
||||
}
|
||||
return nextValue
|
||||
}, initial)
|
||||
|
||||
export const applyPropHooks = (hooks, ...args) =>
|
||||
hooks.reduce((prev, next) => mergeProps(prev, next(...args)), {})
|
||||
@@ -285,6 +293,24 @@ export function isFunction(a) {
|
||||
}
|
||||
}
|
||||
|
||||
export function flattenBy(columns, childKey) {
|
||||
const flatColumns = []
|
||||
|
||||
const recurse = columns => {
|
||||
columns.forEach(d => {
|
||||
if (!d[childKey]) {
|
||||
flatColumns.push(d)
|
||||
} else {
|
||||
recurse(d[childKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
recurse(columns)
|
||||
|
||||
return flatColumns
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function makePathArray(obj) {
|
||||
|
||||
Reference in New Issue
Block a user