diff --git a/README.md b/README.md index 81c0037..29b578e 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,7 @@ Hooks for building **lightweight, fast and extendable datagrids** for React - Extensible via hooks - "Why I wrote React Table and the problems it has solved for Nozzle.io" 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. diff --git a/examples/sorting-client-side/src/App.js b/examples/sorting-client-side/src/App.js index 12d50d1..9701d30 100644 --- a/examples/sorting-client-side/src/App.js +++ b/examples/sorting-client-side/src/App.js @@ -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 ( - - - {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => ( - // Add the sorting props to control sorting. For this example - // we can add them into the header props - - ))} - - ))} - - - {rows.map( - (row, i) => - prepareRow(row) || ( - - {row.cells.map(cell => { - return - })} - - ) - )} - -
- {column.render('Header')} - {/* Add a sort direction indicator */} - - {column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''} - -
{cell.render('Cell')}
+ <> + + + {headerGroups.map(headerGroup => ( + + {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 + + ) + )} + + ))} + + + {firstPageRows.map( + (row, i) => + prepareRow(row) || ( + + {row.cells.map(cell => { + return ( + + ) + })} + + ) + )} + +
+ {column.render('Header')} + {/* Add a sort direction indicator */} + + {column.sorted + ? column.sortedDesc + ? ' 🔽' + : ' 🔼' + : ''} + +
{cell.render('Cell')}
+
+
Showing the first 20 results of {rows.length} rows
+ ) } @@ -118,7 +143,7 @@ function App() { [] ) - const data = React.useMemo(() => makeData(20), []) + const data = React.useMemo(() => makeData(1000), []) return ( diff --git a/src/filterTypes.js b/src/filterTypes.js index ec9b371..cfa1ec2 100644 --- a/src/filterTypes.js +++ b/src/filterTypes.js @@ -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') diff --git a/src/hooks/tests/useTable.test.js b/src/hooks/tests/useTable.test.js new file mode 100644 index 0000000..96c1f94 --- /dev/null +++ b/src/hooks/tests/useTable.test.js @@ -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 ( + + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + + ))} + + ))} + + + {rows.map( + (row, i) => + prepareRow(row) || ( + + {row.cells.map(cell => { + return + })} + + ) + )} + +
{column.render('Header')}
{cell.render('Cell')}
+ ) +} + +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 +} + +test('renders a basic table', () => { + const { getByText } = render() + + 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() +}) diff --git a/src/hooks/useTable.js b/src/hooks/useTable.js index 028d064..a54aa0d 100755 --- a/src/hooks/useTable.js +++ b/src/hooks/useTable.js @@ -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 } diff --git a/src/plugin-hooks/useExpanded.js b/src/plugin-hooks/useExpanded.js index 8fa8e0e..436b578 100755 --- a/src/plugin-hooks/useExpanded.js +++ b/src/plugin-hooks/useExpanded.js @@ -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) diff --git a/src/plugin-hooks/useFilters.js b/src/plugin-hooks/useFilters.js index 4f3d8e5..d4b1197 100755 --- a/src/plugin-hooks/useFilters.js +++ b/src/plugin-hooks/useFilters.js @@ -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, diff --git a/src/plugin-hooks/useGroupBy.js b/src/plugin-hooks/useGroupBy.js index 6360a15..aefe02f 100755 --- a/src/plugin-hooks/useGroupBy.js +++ b/src/plugin-hooks/useGroupBy.js @@ -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( () => { diff --git a/src/plugin-hooks/useSortBy.js b/src/plugin-hooks/useSortBy.js index 74192e0..cf68863 100755 --- a/src/plugin-hooks/useSortBy.js +++ b/src/plugin-hooks/useSortBy.js @@ -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, diff --git a/src/utils.js b/src/utils.js index 3534a6b..f1e0658 100755 --- a/src/utils.js +++ b/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) {