Relocate columns and row logic, fix columns and useGroupBy to be more pure

Since useColumns was relying on groupBy logic, this was code smell. I wanted useGroupBy to be able to add that logic all by itself and not have to have dependencies in the core of the table.

To fix that, I've moved the core column and row logic to the useTable hook and added a new hook 'columnsBeforeHeaderGroups' to allow useGroupBy to do what i needs in a more pure way.
This commit is contained in:
tannerlinsley 2019-07-29 11:00:07 -06:00
parent cadd8bf62a
commit dc73347003
9 changed files with 587 additions and 451 deletions

289
README.md
View File

@ -167,7 +167,7 @@ import {
- [Basic](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/basic)
- [Sorting - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/sorting-client-side)
- [Filtering - Client Side`](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/filtering-client-side)
- [Filtering - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/filtering-client-side)
# Concepts
@ -286,9 +286,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 }`
- `useColumns: Function`
- Optional
- This hook overrides the internal `useColumns` hooks used by `useTable`. You probably never want to override this unless you are testing or developing new features for React Table
- `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
@ -512,6 +509,11 @@ function MyTable({ columns, data }) {
`useSortBy` is the hook that implements **row sorting**. It also support multi-sort (keyboard required).
- Multi-sort is enabled by default
- To sort the table via UI, attach the props generated from each column's `getSortByToggleProps()`, then click any of those elements.
- To multi-sort the table via UI, hold `shift` while clicking on any of those same elements that have the props from `getSortByToggleProps()` attached.
- To programmatically sort (or multi-sort) any column, use the `toggleSortBy` method located on the instance or each individual column.
### Table Options
The following options are supported via the main options object passed to `useTable(options)`
@ -540,14 +542,20 @@ The following options are supported via the main options object passed to `useTa
### `Column` Options
The following options are supported on any `Column` object passed to the `columns` options in `useTable()`
- `disableSorting: Bool`
- Optional
- Defualts to `false`
- If set to `true`, the sorting for this column will be disabled
- `sortDescFirst: Bool`
- Optional
- Defaults to `false`
- If true, the first sort direction for this column will be descending instead of ascending
- If set to `true`, the first sort direction for this column will be descending instead of ascending
- `sortInverted: Bool`
- Optional
- Defaults to `false`
- If true, the underlying sorting direction will be inverted, but the UI will not.
- If set to `true`, the underlying sorting direction will be inverted, but the UI will not.
- This may be useful in situations where positive and negative connotation is inverted, eg. a Golfing score where a lower score is considered more positive than a higher one.
- `sortType: String | Function`
- If a **function** is passed, it must be **memoized**
@ -557,79 +565,95 @@ The following options are supported via the main options object passed to `useTa
- If a `function` is passed, it will be used.
- For mor information on sort types, see [Sorting](TODO)
### Instance Variables
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **sorted** rows.
### Example
```js
const state = useTableState({ sortBy: [{ id: 'firstName', desc: true }] })
const { rows } = useTable(
{
// state[0].sortBy === [{ id: 'firstName', desc: true }]
state,
},
useSortBy
)
```
## `useGroupBy`
- Plugin Hook
- Optional
`useGroupBy` is the hook that implements **row grouping and aggregation**.
### Table Options
The following options are supported via the main options object passed to `useTable(options)`
- `state[0].groupBy: Array<String>`
- Must be **memoized**
- An array of groupBy ID strings, controlling which columns are used to calculate row grouping and aggregation. This information is stored in state since the table is allowed to manipulate the groupBy through user interaction.
- `groupByFn: Function`
- Must be **memoized**
- Defaults to [`defaultGroupByFn`](TODO)
- This function is responsible for grouping rows based on the `state.groupBy` keys provided. It's very rare you would need to customize this function.
- `manualGroupBy: Bool`
- Enables groupBy detection and functionality, but does not automatically perform row grouping.
- Turn this on if you wish to implement your own row grouping outside of the table (eg. server-side or manual row grouping/nesting)
- `disableGrouping: Bool`
- Disables groupBy for the entire table.
- `aggregations: Object<aggregationKey: aggregationFn>`
- Must be **memoized**
- Allows overriding or adding additional aggregation functions for use when grouping/aggregating row values. If an aggregation key isn't found on this object, it will default to using the [built-in aggregation functions](TODO)
### `Instance` Properties
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **grouped and aggregated** rows.
- An array of **sorted** rows.
- `preSortedRows: Array<Row>`
- The array of rows that were originally sorted.
- `toggleSortBy: Function(ColumnID: String, descending: Bool, isMulti: Bool) => void`
- This function can be used to programmatically toggle the sorting for any specific column
### `Column` Properties
The following properties are available on every `Column` object returned by the table instance.
- `canSortBy: 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.
- This function is similar to the `instance`-level `toggleSortBy`, however, passing a columnID is not required since it is located on a `Column` object already.
- `getSortByToggleProps: Function(props) => props`
- **Required**
- This function is used to resolve any props needed for this column's UI that is responsible for toggling the sort direction when the user clicks it.
- You can use the `getSortByToggleProps` hook to extend its functionality.
- Custom props may be passed. **NOTE: Custom props may override built-in sortBy props, so be careful!**
- `sorted: Boolean`
- Denotes whether this column is currently being sorted
- `sortedIndex: Int`
- If the column is currently sorted, this integer will be the index in the `sortBy` array from state that corresponds to this column.
- If this column is not sorted, the index will always be `-1`
- `sortedDesc: Bool`
- If the column is currently sorted, this denotes whether the column's sort direction is descending or not.
- If `true`, the column is sorted `descending`
- If `false`, the column is sorted `ascending`
- If `undefined`, the column is not currently being sorted.
### Example
```js
const state = useTableState({ groupBy: ['firstName'] })
function Table({ columns, data }) {
// Set some default sorting state
const state = useTableState({ sortBy: [{ id: 'firstName', desc: true }] })
const aggregations = React.useMemo(() => ({
customSum: (values, rows) => values.reduce((sum, next) => sum + next, 0),
}))
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
},
useSortBy // Use the sortBy hook
)
const { rows } = useTable(
{
state, // state[0].groupBy === ['firstName']
manualGroupBy: false,
disableGrouping: false,
aggregations,
},
useGroupBy
)
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')}
<span>
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
{/* Add a sort index indicator */}
<span>({column.sorted ? column.sortedIndex + 1 : ''})</span>
</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>
)
}
```
## `useFilters`
@ -663,6 +687,27 @@ The following options are supported via the main options object passed to `useTa
- Allows overriding or adding additional filter types for columns to use. If a column's filter type isn't found on this object, it will default to using the [built-in filter types](TODO).
- For mor information on filter types, see [Filtering](TODO)
### `Column` Options
The following options are supported on any `Column` object passed to the `columns` options in `useTable()`
- `Filter: Function | React.Component => JSX`
- **Required**
- Receives the table instance and column model as props
- Must return valid JSX
- This function (or component) is used to render this column's filter UI, eg.
- `disableFilters: Bool`
- Optional
- If set to `true`, will disable filtering for this column
- `filter: String | Function`
- Optional
- Defaults to [`text`](TODO)
- The resolved function from the this string/function will be used to filter the this column's data.
- If a `string` is passed, the function with that name located on either the custom `filterTypes` option or the built-in filtering types object will be used. If
- If a `function` is passed, it will be used directly.
- For mor information on filter types, see [Filtering](TODO)
- If a **function** is passed, it must be **memoized**
### `Instance` Properties
The following values are provided to the table `instance`:
@ -677,6 +722,20 @@ The following values are provided to the table `instance`:
- `setAllFilters: Function(filtersObject) => void`
- An instance-level function used to update the values for **all** filters on the table, all at once.
### `Column` Properties
The following properties are available on every `Column` object returned by the table instance.
- `canFilter: Bool`
- Denotes whether a column is filterable or not depending on if it has a valid accessor/data model or is manually disabled via an option.
- `setFilter: Function(filterValue) => void`
- An column-level function used to update the filter value for this column
- `filterValue: any`
- The current filter value for this column, resolved from the table state's `filters` object
- `preFilteredRows: Array<row>`
- The array of rows that were originally passed to this columns filter **before** they were filtered.
- This array of rows can be useful if building faceted filter options.
### Example
```js
@ -723,6 +782,100 @@ const { rows } = useTable(
)
```
## `useGroupBy`
- Plugin Hook
- Optional
`useGroupBy` is the hook that implements **row grouping and aggregation**.
- Each column's `getGroupByToggleProps()` function can be used to generate the props needed to make a clickable UI element that will toggle the grouping on or off for a specific column.
- Instance and column-level `toggleGroupBy` functions are also made available for programmatic grouping.
### Table Options
The following options are supported via the main options object passed to `useTable(options)`
- `state[0].groupBy: Array<String>`
- Must be **memoized**
- An array of groupBy ID strings, controlling which columns are used to calculate row grouping and aggregation. This information is stored in state since the table is allowed to manipulate the groupBy through user interaction.
- `groupByFn: Function`
- Must be **memoized**
- Defaults to [`defaultGroupByFn`](TODO)
- This function is responsible for grouping rows based on the `state.groupBy` keys provided. It's very rare you would need to customize this function.
- `manualGroupBy: Bool`
- Enables groupBy detection and functionality, but does not automatically perform row grouping.
- Turn this on if you wish to implement your own row grouping outside of the table (eg. server-side or manual row grouping/nesting)
- `disableGrouping: Bool`
- Disables groupBy for the entire table.
- `aggregations: Object<aggregationKey: aggregationFn>`
- Must be **memoized**
- Allows overriding or adding additional aggregation functions for use when grouping/aggregating row values. If an aggregation key isn't found on this object, it will default to using the [built-in aggregation functions](TODO)
### `Column` Options
The following options are supported on any `Column` object passed to the `columns` options in `useTable()`
- `Aggregated: Function | React.Component => JSX`
- Optional
- Defaults to this column's `Cell` formatter
- Receives the table instance and cell model as props
- Must return valid JSX
- This function (or component) formats this column's value when it is being grouped and aggregated, eg. If this column was showing the number of visits for a user to a website and it was currently being grouped to show an **average** of the values, the `Aggregated` function for this column could format that value to `1,000 Avg. Visits`
- `disableGrouping: Boolean`
- Defaults to `true`
- If `true`, this column is able to be grouped.
### `Instance` Properties
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **grouped and aggregated** rows.
- `preGroupedRows: Array<Row>`
- The array of rows originally used to create the grouped rows.
- `toggleGroupBy: Function(columnID: String, ?set: Bool) => void`
- This function can be used to programmatically set or toggle the groupBy state for a specific column.
### `Column` Properties
The following properties are available on every `Column` object returned by the table instance.
- `canGroupBy: Boolean`
- If `true`, this column is able to be grouped.
- This is resolved from the column having a valid accessor / data model, and not being manually disabled via other `useGroupBy` related options
- `grouped: Boolean`
- If `true`, this column is currently being grouped
- `groupedIndex: Int`
- If this column is currently being grouped, this integer is the index of this column's ID in the table state's `groupBy` array.
- `toggleGroupBy: Function(?set: Bool) => void`
- This function can be used to programmatically set or toggle the groupBy state fo this column.
- `getGroupByToggleProps: Function(props) => props`
- **Required**
- This function is used to resolve any props needed for this column's UI that is responsible for toggling grouping when the user clicks it.
- You can use the `getGroupByToggleProps` hook to extend its functionality.
- Custom props may be passed. **NOTE: Custom props may override built-in sortBy props, so be careful!**
### Example
```js
const state = useTableState({ groupBy: ['firstName'] })
const aggregations = React.useMemo(() => ({
customSum: (values, rows) => values.reduce((sum, next) => sum + next, 0),
}))
const { rows } = useTable(
{
state, // state[0].groupBy === ['firstName']
manualGroupBy: false,
disableGrouping: false,
aggregations,
},
useGroupBy
)
```
## `useExpanded`
- Plugin Hook

View File

@ -1,229 +0,0 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { getBy } from '../utils'
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
Cell: PropTypes.any,
Header: PropTypes.any,
})
),
defaultColumn: PropTypes.any,
}
// Find the depth of the columns
function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
function decorateColumn(column, defaultColumn, parent, depth, index) {
// Apply the defaultColumn
column = { ...defaultColumn, ...column }
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorString = accessor
accessor = row => getBy(row, accessorString)
}
if (!id && typeof Header === 'string') {
id = Header
}
if (!id && column.columns) {
console.error(column)
throw new Error('A column ID (or unique "Header" value) is required!')
}
if (!id) {
console.error(column)
throw new Error('A column ID (or string accessor) is required!')
}
column = {
Header: ({ id }) => id,
Cell: ({ value }) => value,
show: true,
...column,
id,
accessor,
parent,
depth,
index,
}
return column
}
// Build the visible columns, headers and flat column list
function decorateColumnTree(columns, defaultColumn, parent, depth = 0) {
return columns.map((column, columnIndex) => {
column = decorateColumn(column, defaultColumn, parent, depth, columnIndex)
if (column.columns) {
column.columns = decorateColumnTree(
column.columns,
defaultColumn,
column,
depth + 1
)
}
return column
})
}
// Build the header groups from the bottom up
function makeHeaderGroups(columns, maxDepth, defaultColumn) {
const headerGroups = []
const removeChildColumns = column => {
delete column.columns
if (column.parent) {
removeChildColumns(column.parent)
}
}
columns.forEach(removeChildColumns)
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: [],
}
const parentColumns = []
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
const isFirst = !parentColumns.length
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_'),
})
}
} else if (hasParents) {
// If other columns have parents, add a place holder if necessary
const placeholderColumn = decorateColumn(
{
originalID: [column.id, 'placeholder', maxDepth - depth].join('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length,
].join('_'),
},
defaultColumn
)
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new columns[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.columns = latestParentColumn.columns || []
if (!latestParentColumn.columns.includes(column)) {
latestParentColumn.columns.push(column)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns)
}
}
buildGroup(columns)
return headerGroups.reverse()
}
export const useColumns = props => {
const {
debug,
columns: userColumns,
defaultColumn = {},
state: [{ groupBy }],
} = props
PropTypes.checkPropTypes(propTypes, props, 'property', 'useColumns')
const { columns, headerGroups, headers } = useMemo(() => {
if (debug) console.info('getColumns')
// Decorate All the columns
let columnTree = decorateColumnTree(userColumns, defaultColumn)
// Get the flat list of all columns
let columns = flattenBy(columnTree, 'columns')
columns = [
...groupBy.map(g => columns.find(col => col.id === g)),
...columns.filter(col => !groupBy.includes(col.id)),
]
// Get headerGroups
const headerGroups = makeHeaderGroups(
columns,
findMaxDepth(columnTree),
defaultColumn
)
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers,
}
}, [debug, defaultColumn, groupBy, userColumns])
return {
...props,
columns,
headerGroups,
headers,
}
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
}
}

View File

@ -1,66 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const propTypes = {
subRowsKey: PropTypes.string,
}
export const useRows = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useRows')
const { debug, columns, subRowsKey = 'subRows', data } = props
const accessedRows = React.useMemo(() => {
if (debug) console.info('getAccessedRows')
// Access the row's data
const accessRow = (originalRow, i, depth = 0) => {
// Keep the original reference around
const original = originalRow
// Process any subRows
const subRows = originalRow[subRowsKey]
? originalRow[subRowsKey].map((d, i) => accessRow(d, i, depth + 1))
: undefined
const row = {
original,
index: i,
path: [i], // used to create a key for each row even if not nested
subRows,
depth,
cells: [{}], // This is a dummy cell
}
// Override common array functions (and the dummy cell's getCellProps function)
// to show an error if it is accessed without calling prepareRow
const unpreparedAccessWarning = () => {
throw new Error(
'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.'
)
}
row.cells.map = unpreparedAccessWarning
row.cells.filter = unpreparedAccessWarning
row.cells.forEach = unpreparedAccessWarning
row.cells[0].getCellProps = unpreparedAccessWarning
// Create the cells and values
row.values = {}
columns.forEach(column => {
row.values[column.id] = column.accessor
? column.accessor(originalRow, i, { subRows, depth, data })
: undefined
})
return row
}
// Use the resolved data
return data.map((d, i) => accessRow(d, i))
}, [debug, data, subRowsKey, columns])
return {
...props,
rows: accessedRows,
}
}

View File

@ -1,14 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
//
import { applyHooks, applyPropHooks, mergeProps, flexRender } from '../utils'
import {
applyHooks,
applyPropHooks,
mergeProps,
flexRender,
decorateColumnTree,
makeHeaderGroups,
findMaxDepth,
} from '../utils'
import { useTableState } from './useTableState'
import { useColumns } from './useColumns'
import { useRows } from './useRows'
const propTypes = {
// General
data: PropTypes.array.isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
Cell: PropTypes.any,
Header: PropTypes.any,
})
).isRequired,
defaultColumn: PropTypes.object,
subRowsKey: PropTypes.string,
debug: PropTypes.bool,
}
@ -23,8 +39,10 @@ export const useTable = (props, ...plugins) => {
let {
data,
state: userState,
useColumns: userUseColumns = useColumns,
useRows: userUseRows = useRows,
columns: userColumns,
defaultColumn = {},
subRowsKey = 'subRows',
debug,
} = props
@ -62,11 +80,100 @@ export const useTable = (props, ...plugins) => {
if (debug) console.time('hooks')
// Loop through plugins to build the api out
api = [userUseColumns, userUseRows, ...plugins]
api = [userUseRows, ...plugins]
.filter(Boolean)
.reduce((prev, next) => next(prev), api)
if (debug) console.timeEnd('hooks')
// Compute columns, headerGroups and headers
const columnInfo = React.useMemo(
() => {
if (debug) console.info('buildColumns/headerGroup/headers')
// Decorate All the columns
let columnTree = decorateColumnTree(userColumns, defaultColumn)
// Get the flat list of all columns
let columns = flattenBy(columnTree, 'columns')
// Allow hooks to decorate columns
if (debug) console.time('hooks.columnsBeforeHeaderGroups')
columns = applyHooks(api.hooks.columnsBeforeHeaderGroups, columns, api)
if (debug) console.timeEnd('hooks.columnsBeforeHeaderGroups')
// Make the headerGroups
const headerGroups = makeHeaderGroups(
columns,
findMaxDepth(columnTree),
defaultColumn
)
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers,
}
},
[api, debug, defaultColumn, userColumns]
)
// Place the columns, headerGroups and headers on the api
Object.assign(api, columnInfo)
// Access the row model
api.rows = React.useMemo(
() => {
if (debug) console.info('getAccessedRows')
// Access the row's data
const accessRow = (originalRow, i, depth = 0) => {
// Keep the original reference around
const original = originalRow
// Process any subRows
const subRows = originalRow[subRowsKey]
? originalRow[subRowsKey].map((d, i) => accessRow(d, i, depth + 1))
: undefined
const row = {
original,
index: i,
path: [i], // used to create a key for each row even if not nested
subRows,
depth,
cells: [{}], // This is a dummy cell
}
// Override common array functions (and the dummy cell's getCellProps function)
// to show an error if it is accessed without calling prepareRow
const unpreparedAccessWarning = () => {
throw new Error(
'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.'
)
}
row.cells.map = unpreparedAccessWarning
row.cells.filter = unpreparedAccessWarning
row.cells.forEach = unpreparedAccessWarning
row.cells[0].getCellProps = unpreparedAccessWarning
// Create the cells and values
row.values = {}
api.columns.forEach(column => {
row.values[column.id] = column.accessor
? column.accessor(originalRow, i, { subRows, depth, data })
: undefined
})
return row
}
// Use the resolved data
return data.map((d, i) => accessRow(d, i))
},
[debug, data, subRowsKey, api.columns]
)
// Determine column visibility
api.columns.forEach(column => {
column.visible =
@ -215,3 +322,21 @@ export const useTable = (props, ...plugins) => {
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
}

View File

@ -1,8 +1,6 @@
import * as utils from './utils'
export { utils }
export { useTable } from './hooks/useTable'
export { useColumns } from './hooks/useColumns'
export { useRows } from './hooks/useRows'
export { useTableState, defaultState } from './hooks/useTableState'
export { useExpanded } from './plugin-hooks/useExpanded'
export { useFilters } from './plugin-hooks/useFilters'

View File

@ -14,14 +14,11 @@ const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
filterFn: PropTypes.func,
filterAll: PropTypes.bool,
canFilter: PropTypes.bool,
disableFilters: PropTypes.bool,
Filter: PropTypes.any,
})
),
filterFn: PropTypes.func,
manualFilters: PropTypes.bool,
}
@ -99,12 +96,12 @@ export const useFilters = props => {
hooks.columns.push(columns => {
columns.forEach(column => {
const { id, accessor, canFilter } = column
const { id, accessor, disableFilters: columnDisableFilters } = column
// Determine if a column is filterable
column.canFilter = accessor
? getFirstDefined(
canFilter,
columnDisableFilters,
disableFilters === true ? false : undefined,
true
)
@ -143,13 +140,12 @@ export const useFilters = props => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
column.preFilteredRows = filteredSoFar
// Don't filter hidden columns or columns that have had their filters disabled
if (!column || column.filterable === false) {
if (!column) {
return filteredSoFar
}
column.preFilteredRows = filteredSoFar
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},

View File

@ -20,12 +20,13 @@ const propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
aggregate: PropTypes.func,
canGroupBy: PropTypes.bool,
disableGrouping: PropTypes.bool,
Aggregated: PropTypes.any,
})
),
groupByFn: PropTypes.func,
manualGrouping: PropTypes.bool,
disableGrouping: PropTypes.bool,
aggregations: PropTypes.object,
}
@ -44,13 +45,23 @@ export const useGroupBy = props => {
state: [{ groupBy }, setState],
} = props
// Sort grouped columns to the start of the column list
// before the headers are built
hooks.columnsBeforeHeaderGroups.push(columns => {
return [
...groupBy.map(g => columns.find(col => col.id === g)),
...columns.filter(col => !groupBy.includes(col.id)),
]
})
columns.forEach(column => {
const { id, accessor, canGroupBy } = column
const { id, accessor, disableGrouping: columnDisableGrouping } = column
column.grouped = groupBy.includes(id)
column.groupedIndex = groupBy.indexOf(id)
column.canGroupBy = accessor
? getFirstDefined(
canGroupBy,
columnDisableGrouping,
disableGrouping === true ? false : undefined,
true
)
@ -115,81 +126,78 @@ export const useGroupBy = props => {
hooks.columns.push(addGroupByToggleProps)
hooks.headers.push(addGroupByToggleProps)
const groupedRows = useMemo(() => {
if (manualGroupBy || !groupBy.length) {
return rows
}
if (debug) console.info('getGroupedRows')
// Find the columns that can or are aggregating
// Uses each column to aggregate rows into a single value
const aggregateRowsToValues = rows => {
const values = {}
columns.forEach(column => {
const columnValues = rows.map(d => d.values[column.id])
let aggregate =
userAggregations[column.aggregate] ||
aggregations[column.aggregate] ||
column.aggregate
if (typeof aggregate === 'function') {
values[column.id] = aggregate(columnValues, rows)
} else if (aggregate) {
throw new Error(
`Invalid aggregate "${aggregate}" passed to column with ID: "${
column.id
}"`
)
} else {
values[column.id] = columnValues[0]
}
})
return values
}
// Recursively group the data
const groupRecursively = (rows, groupBy, depth = 0) => {
// This is the last level, just return the rows
if (depth >= groupBy.length) {
const groupedRows = useMemo(
() => {
if (manualGroupBy || !groupBy.length) {
return rows
}
if (debug) console.info('getGroupedRows')
// Find the columns that can or are aggregating
// Group the rows together for this level
let groupedRows = Object.entries(groupByFn(rows, groupBy[depth])).map(
([groupByVal, subRows], index) => {
// Recurse to sub rows before aggregation
subRows = groupRecursively(subRows, groupBy, depth + 1)
const values = aggregateRowsToValues(subRows)
const row = {
groupByID: groupBy[depth],
groupByVal,
values,
subRows,
depth,
index,
// Uses each column to aggregate rows into a single value
const aggregateRowsToValues = rows => {
const values = {}
columns.forEach(column => {
const columnValues = rows.map(d => d.values[column.id])
let aggregate =
userAggregations[column.aggregate] ||
aggregations[column.aggregate] ||
column.aggregate
if (typeof aggregate === 'function') {
values[column.id] = aggregate(columnValues, rows)
} else if (aggregate) {
throw new Error(
`Invalid aggregate "${aggregate}" passed to column with ID: "${
column.id
}"`
)
} else {
values[column.id] = columnValues[0]
}
return row
})
return values
}
// Recursively group the data
const groupRecursively = (rows, groupBy, depth = 0) => {
// This is the last level, just return the rows
if (depth >= groupBy.length) {
return rows
}
)
return groupedRows
}
// Group the rows together for this level
let groupedRows = Object.entries(groupByFn(rows, groupBy[depth])).map(
([groupByVal, subRows], index) => {
// Recurse to sub rows before aggregation
subRows = groupRecursively(subRows, groupBy, depth + 1)
// Assign the new data
return groupRecursively(rows, groupBy)
}, [
manualGroupBy,
groupBy,
debug,
rows,
columns,
userAggregations,
groupByFn,
])
const values = aggregateRowsToValues(subRows)
const row = {
groupByID: groupBy[depth],
groupByVal,
values,
subRows,
depth,
index,
}
return row
}
)
return groupedRows
}
// Assign the new data
return groupRecursively(rows, groupBy)
},
[manualGroupBy, groupBy, debug, rows, columns, userAggregations, groupByFn]
)
return {
...props,
toggleGroupBy,
rows: groupedRows,
preGroupedRows: rows,
}
}

View File

@ -22,6 +22,7 @@ const propTypes = {
PropTypes.shape({
sortType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
sortDescFirst: PropTypes.bool,
disableSorting: PropTypes.bool,
})
),
orderByFn: PropTypes.func,
@ -67,10 +68,10 @@ export const useSortBy = props => {
}
columns.forEach(column => {
const { accessor, canSortBy } = column
const { accessor, disableSorting: columnDisableSorting } = column
column.canSortBy = accessor
? getFirstDefined(
canSortBy,
columnDisableSorting,
disableSorting === true ? false : undefined,
true
)
@ -78,7 +79,7 @@ export const useSortBy = props => {
})
// Updates sorting based on a columnID, desc flag and multi flag
const toggleSortByID = (columnID, desc, multi) => {
const toggleSortBy = (columnID, desc, multi) => {
return setState(old => {
const { sortBy } = old
@ -167,7 +168,7 @@ export const useSortBy = props => {
columns.forEach(column => {
if (column.canSortBy) {
column.toggleSortBy = (desc, multi) =>
toggleSortByID(column.id, desc, multi)
toggleSortBy(column.id, desc, multi)
}
})
return columns
@ -279,6 +280,8 @@ export const useSortBy = props => {
return {
...props,
toggleSortBy,
rows: sortedRows,
preSortedRows: rows,
}
}

View File

@ -1,5 +1,153 @@
import React from 'react'
// Find the depth of the columns
export function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
export function decorateColumn(column, defaultColumn, parent, depth, index) {
// Apply the defaultColumn
column = { ...defaultColumn, ...column }
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorString = accessor
accessor = row => getBy(row, accessorString)
}
if (!id && typeof Header === 'string') {
id = Header
}
if (!id && column.columns) {
console.error(column)
throw new Error('A column ID (or unique "Header" value) is required!')
}
if (!id) {
console.error(column)
throw new Error('A column ID (or string accessor) is required!')
}
column = {
Header: ({ id }) => id,
Cell: ({ value }) => value,
show: true,
...column,
id,
accessor,
parent,
depth,
index,
}
return column
}
// Build the visible columns, headers and flat column list
export function decorateColumnTree(columns, defaultColumn, parent, depth = 0) {
return columns.map((column, columnIndex) => {
column = decorateColumn(column, defaultColumn, parent, depth, columnIndex)
if (column.columns) {
column.columns = decorateColumnTree(
column.columns,
defaultColumn,
column,
depth + 1
)
}
return column
})
}
// Build the header groups from the bottom up
export function makeHeaderGroups(columns, maxDepth, defaultColumn) {
const headerGroups = []
const removeChildColumns = column => {
delete column.columns
if (column.parent) {
removeChildColumns(column.parent)
}
}
columns.forEach(removeChildColumns)
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: [],
}
const parentColumns = []
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
const isFirst = !parentColumns.length
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_'),
})
}
} else if (hasParents) {
// If other columns have parents, add a place holder if necessary
const placeholderColumn = decorateColumn(
{
originalID: [column.id, 'placeholder', maxDepth - depth].join('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length,
].join('_'),
},
defaultColumn
)
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new columns[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.columns = latestParentColumn.columns || []
if (!latestParentColumn.columns.includes(column)) {
latestParentColumn.columns.push(column)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns)
}
}
buildGroup(columns)
return headerGroups.reverse()
}
export function getBy(obj, path, def) {
if (!path) {
return obj