UNPKG

react-table

Version:

Hooks for building lightweight, fast and extendable datagrids for React

439 lines (374 loc) 11.3 kB
import React from 'react' import * as aggregations from '../aggregations' import { getFirstDefined, flattenBy } from '../utils' import { actions, makePropGetter, ensurePluginOrder, useMountedLayoutEffect, useGetLatest, } from '../publicUtils' const emptyArray = [] const emptyObject = {} // Actions actions.resetGroupBy = 'resetGroupBy' actions.setGroupBy = 'setGroupBy' actions.toggleGroupBy = 'toggleGroupBy' export const useGroupBy = hooks => { hooks.getGroupByToggleProps = [defaultGetGroupByToggleProps] hooks.stateReducers.push(reducer) hooks.visibleColumnsDeps.push((deps, { instance }) => [ ...deps, instance.state.groupBy, ]) hooks.visibleColumns.push(visibleColumns) hooks.useInstance.push(useInstance) hooks.prepareRow.push(prepareRow) } useGroupBy.pluginName = 'useGroupBy' const defaultGetGroupByToggleProps = (props, { header }) => [ props, { onClick: header.canGroupBy ? e => { e.persist() header.toggleGroupBy() } : undefined, style: { cursor: header.canGroupBy ? 'pointer' : undefined, }, title: 'Toggle GroupBy', }, ] // Reducer function reducer(state, action, previousState, instance) { if (action.type === actions.init) { return { groupBy: [], ...state, } } if (action.type === actions.resetGroupBy) { return { ...state, groupBy: instance.initialState.groupBy || [], } } if (action.type === actions.setGroupBy) { const { value } = action return { ...state, groupBy: value, } } if (action.type === actions.toggleGroupBy) { const { columnId, value: setGroupBy } = action const resolvedGroupBy = typeof setGroupBy !== 'undefined' ? setGroupBy : !state.groupBy.includes(columnId) if (resolvedGroupBy) { return { ...state, groupBy: [...state.groupBy, columnId], } } return { ...state, groupBy: state.groupBy.filter(d => d !== columnId), } } } function visibleColumns( columns, { instance: { state: { groupBy }, }, } ) { // Sort grouped columns to the start of the column list // before the headers are built const groupByColumns = groupBy .map(g => columns.find(col => col.id === g)) .filter(Boolean) const nonGroupByColumns = columns.filter(col => !groupBy.includes(col.id)) columns = [...groupByColumns, ...nonGroupByColumns] columns.forEach(column => { column.isGrouped = groupBy.includes(column.id) column.groupedIndex = groupBy.indexOf(column.id) }) return columns } const defaultUserAggregations = {} function useInstance(instance) { const { data, rows, flatRows, rowsById, allColumns, flatHeaders, groupByFn = defaultGroupByFn, manualGroupBy, aggregations: userAggregations = defaultUserAggregations, plugins, state: { groupBy }, dispatch, autoResetGroupBy = true, disableGroupBy, defaultCanGroupBy, getHooks, } = instance ensurePluginOrder(plugins, ['useColumnOrder', 'useFilters'], 'useGroupBy') const getInstance = useGetLatest(instance) allColumns.forEach(column => { const { accessor, defaultGroupBy: defaultColumnGroupBy, disableGroupBy: columnDisableGroupBy, } = column column.canGroupBy = accessor ? getFirstDefined( column.canGroupBy, columnDisableGroupBy === true ? false : undefined, disableGroupBy === true ? false : undefined, true ) : getFirstDefined( column.canGroupBy, defaultColumnGroupBy, defaultCanGroupBy, false ) if (column.canGroupBy) { column.toggleGroupBy = () => instance.toggleGroupBy(column.id) } column.Aggregated = column.Aggregated || column.Cell }) const toggleGroupBy = React.useCallback( (columnId, value) => { dispatch({ type: actions.toggleGroupBy, columnId, value }) }, [dispatch] ) const setGroupBy = React.useCallback( value => { dispatch({ type: actions.setGroupBy, value }) }, [dispatch] ) flatHeaders.forEach(header => { header.getGroupByToggleProps = makePropGetter( getHooks().getGroupByToggleProps, { instance: getInstance(), header } ) }) const [ groupedRows, groupedFlatRows, groupedRowsById, onlyGroupedFlatRows, onlyGroupedRowsById, nonGroupedFlatRows, nonGroupedRowsById, ] = React.useMemo(() => { if (manualGroupBy || !groupBy.length) { return [ rows, flatRows, rowsById, emptyArray, emptyObject, flatRows, rowsById, ] } // Ensure that the list of filtered columns exist const existingGroupBy = groupBy.filter(g => allColumns.find(col => col.id === g) ) // Find the columns that can or are aggregating // Uses each column to aggregate rows into a single value const aggregateRowsToValues = (leafRows, groupedRows, depth) => { const values = {} allColumns.forEach(column => { // Don't aggregate columns that are in the groupBy if (existingGroupBy.includes(column.id)) { values[column.id] = groupedRows[0] ? groupedRows[0].values[column.id] : null return } // Aggregate the values let aggregateFn = typeof column.aggregate === 'function' ? column.aggregate : userAggregations[column.aggregate] || aggregations[column.aggregate] if (aggregateFn) { // Get the columnValues to aggregate const groupedValues = groupedRows.map(row => row.values[column.id]) // Get the columnValues to aggregate const leafValues = leafRows.map(row => { let columnValue = row.values[column.id] if (!depth && column.aggregateValue) { const aggregateValueFn = typeof column.aggregateValue === 'function' ? column.aggregateValue : userAggregations[column.aggregateValue] || aggregations[column.aggregateValue] if (!aggregateValueFn) { console.info({ column }) throw new Error( `React Table: Invalid column.aggregateValue option for column listed above` ) } columnValue = aggregateValueFn(columnValue, row, column) } return columnValue }) values[column.id] = aggregateFn(leafValues, groupedValues) } else if (column.aggregate) { console.info({ column }) throw new Error( `React Table: Invalid column.aggregate option for column listed above` ) } else { values[column.id] = null } }) return values } let groupedFlatRows = [] const groupedRowsById = {} const onlyGroupedFlatRows = [] const onlyGroupedRowsById = {} const nonGroupedFlatRows = [] const nonGroupedRowsById = {} // Recursively group the data const groupUpRecursively = (rows, depth = 0, parentId) => { // This is the last level, just return the rows if (depth === existingGroupBy.length) { return rows.map((row) => ({ ...row, depth })) } const columnId = existingGroupBy[depth] // Group the rows together for this level let rowGroupsMap = groupByFn(rows, columnId) // Peform aggregations for each group const aggregatedGroupedRows = Object.entries(rowGroupsMap).map( ([groupByVal, groupedRows], index) => { let id = `${columnId}:${groupByVal}` id = parentId ? `${parentId}>${id}` : id // First, Recurse to group sub rows before aggregation const subRows = groupUpRecursively(groupedRows, depth + 1, id) // Flatten the leaf rows of the rows in this group const leafRows = depth ? flattenBy(groupedRows, 'leafRows') : groupedRows const values = aggregateRowsToValues(leafRows, groupedRows, depth) const row = { id, isGrouped: true, groupByID: columnId, groupByVal, values, subRows, leafRows, depth, index, } subRows.forEach(subRow => { groupedFlatRows.push(subRow) groupedRowsById[subRow.id] = subRow if (subRow.isGrouped) { onlyGroupedFlatRows.push(subRow) onlyGroupedRowsById[subRow.id] = subRow } else { nonGroupedFlatRows.push(subRow) nonGroupedRowsById[subRow.id] = subRow } }) return row } ) return aggregatedGroupedRows } const groupedRows = groupUpRecursively(rows) groupedRows.forEach(subRow => { groupedFlatRows.push(subRow) groupedRowsById[subRow.id] = subRow if (subRow.isGrouped) { onlyGroupedFlatRows.push(subRow) onlyGroupedRowsById[subRow.id] = subRow } else { nonGroupedFlatRows.push(subRow) nonGroupedRowsById[subRow.id] = subRow } }) // Assign the new data return [ groupedRows, groupedFlatRows, groupedRowsById, onlyGroupedFlatRows, onlyGroupedRowsById, nonGroupedFlatRows, nonGroupedRowsById, ] }, [ manualGroupBy, groupBy, rows, flatRows, rowsById, allColumns, userAggregations, groupByFn, ]) const getAutoResetGroupBy = useGetLatest(autoResetGroupBy) useMountedLayoutEffect(() => { if (getAutoResetGroupBy()) { dispatch({ type: actions.resetGroupBy }) } }, [dispatch, manualGroupBy ? null : data]) Object.assign(instance, { preGroupedRows: rows, preGroupedFlatRow: flatRows, preGroupedRowsById: rowsById, groupedRows, groupedFlatRows, groupedRowsById, onlyGroupedFlatRows, onlyGroupedRowsById, nonGroupedFlatRows, nonGroupedRowsById, rows: groupedRows, flatRows: groupedFlatRows, rowsById: groupedRowsById, toggleGroupBy, setGroupBy, }) } function prepareRow(row) { row.allCells.forEach(cell => { // Grouped cells are in the groupBy and the pivot cell for the row cell.isGrouped = cell.column.isGrouped && cell.column.id === row.groupByID // Placeholder cells are any columns in the groupBy that are not grouped cell.isPlaceholder = !cell.isGrouped && cell.column.isGrouped // Aggregated cells are not grouped, not repeated, but still have subRows cell.isAggregated = !cell.isGrouped && !cell.isPlaceholder && row.subRows?.length }) } export function defaultGroupByFn(rows, columnId) { return rows.reduce((prev, row, i) => { // TODO: Might want to implement a key serializer here so // irregular column values can still be grouped if needed? const resKey = `${row.values[columnId]}` prev[resKey] = Array.isArray(prev[resKey]) ? prev[resKey] : [] prev[resKey].push(row) return prev }, {}) }