Rename hook files

This commit is contained in:
Tanner Linsley
2019-02-11 09:33:34 -07:00
parent 2d31d3bac9
commit a0495f6858
32 changed files with 3431 additions and 4221 deletions

11
.babelrc Normal file
View File

@@ -0,0 +1,11 @@
{
"presets": [
[
"@babel/env",
{
"modules": false
}
],
"@babel/react"
]
}

15
.eslintrc Normal file
View File

@@ -0,0 +1,15 @@
{
"parser": "babel-eslint",
"extends": ["standard", "standard-react"],
"env": {
"es6": true
},
"plugins": ["react"],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"space-before-function-paren": 0,
"react/jsx-boolean-value": 0
}
}

View File

@@ -1,3 +0,0 @@
module.exports = {
extends: 'react-tools',
}

2
dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/index.js.zip vendored Normal file

Binary file not shown.

View File

@@ -1,42 +1,32 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import babel from 'rollup-plugin-babel'
import replace from 'rollup-plugin-replace'
import commonjs from 'rollup-plugin-commonjs'
import uglify from 'rollup-plugin-uglify'
import external from 'rollup-plugin-peer-deps-external'
import resolve from 'rollup-plugin-node-resolve'
import { uglify } from 'rollup-plugin-uglify'
const env = process.env.NODE_ENV
import pkg from './package.json'
const config = {
export default {
input: 'src/index.js',
output: {
file: env === 'production' ? 'react-table.min.js' : 'react-table.js',
format: 'umd',
globals: {
react: 'React',
},
name: 'ReactTable',
exports: 'named',
},
external: ['react'],
plugins: [
nodeResolve(),
babel({
exclude: '**/node_modules/**',
}),
replace({
'process.env.NODE_ENV': JSON.stringify(env),
}),
commonjs(),
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true
}
// {
// file: pkg.module,
// format: "es",
// sourcemap: true
// }
],
plugins: [
external(),
babel({
exclude: 'node_modules/**'
}),
resolve(),
commonjs(),
uglify()
]
}
if (env === 'production') {
config.plugins.push(uglify({
compress: {
dead_code: true,
warnings: false,
},
}))
}
export default config

9
src/actions.js Executable file
View File

@@ -0,0 +1,9 @@
const actions = {}
export { actions }
export const addActions = acts => {
Object.keys(acts).forEach(key => {
actions[key] = acts[key]
})
}

View File

@@ -1,7 +1,7 @@
export function sum (values, rows) {
export function sum(values, rows) {
return values.reduce((sum, next) => sum + next, 0)
}
export function average (values, rows) {
export function average(values, rows) {
return Math.round((sum(values, rows) / values.length) * 100) / 100
}

View File

@@ -1,41 +0,0 @@
import { useMemo } from 'react'
export default function useAccessedRows ({
debug, data, columns, subRowsKey,
}) {
return useMemo(
() => {
if (debug) console.info('getAccessedRows')
// Access the row's data
const accessRow = (data, i, depth = 0) => {
// Keep the original reference around
const original = data
// Process any subRows
const subRows = data[subRowsKey]
? data[subRowsKey].map((d, i) => accessRow(d, i, depth + 1))
: undefined
const row = {
original,
index: i,
subRows,
depth,
}
// Create the cells and values
row.values = {}
columns.forEach(column => {
row.values[column.id] = column.accessor ? column.accessor(data) : undefined
})
return row
}
// Use the resolved data
return data.map((d, i) => accessRow(d, i))
},
[data, columns]
)
}

View File

@@ -1,45 +1,61 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { getFirstDefined, getBy } from '../utils'
import { getBy } from '../utils'
export default function useColumns ({
debug,
groupBy,
userColumns,
disableSorting,
disableGrouping,
disableFilters,
}) {
return useMemo(
() => {
if (debug) console.info('getColumns')
// Decorate All the columns
const columnTree = decorateColumnTree(userColumns)
// 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))
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers,
}
},
[groupBy, userColumns]
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
Cell: PropTypes.any,
Header: PropTypes.any
})
)
}
export const useColumns = props => {
const {
debug,
columns: userColumns,
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)
// 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))
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers
}
}, [groupBy, userColumns])
return {
...props,
columns,
headerGroups,
headers
}
// Find the depth of the columns
function findMaxDepth (columns, depth = 0) {
function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
@@ -48,11 +64,9 @@ export default function useColumns ({
}, 0)
}
function decorateColumn (column, parent) {
function decorateColumn(column, parent) {
// First check for string accessor
let {
id, accessor, Header, canSortBy, canGroupBy, canFilter,
} = column
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
@@ -60,18 +74,6 @@ export default function useColumns ({
accessor = row => getBy(row, accessorString)
}
const grouped = groupBy.includes(id)
canSortBy = accessor
? getFirstDefined(canSortBy, disableSorting === true ? false : undefined, true)
: false
canGroupBy = accessor
? getFirstDefined(canGroupBy, disableGrouping === true ? false : undefined, true)
: false
canFilter = accessor
? getFirstDefined(canFilter, disableFilters === true ? false : undefined, true)
: false
if (!id && typeof Header === 'string') {
id = Header
}
@@ -87,22 +89,16 @@ export default function useColumns ({
Cell: cell => cell.value,
show: true,
...column,
grouped,
canSortBy,
canGroupBy,
canFilter,
id,
accessor,
parent,
parent
}
column.Aggregated = column.Aggregated || column.Cell
return column
}
// Build the visible columns, headers and flat column list
function decorateColumnTree (columns, parent, depth = 0) {
function decorateColumnTree(columns, parent, depth = 0) {
return columns.map(column => {
column = decorateColumn(column, parent)
if (column.columns) {
@@ -112,7 +108,7 @@ export default function useColumns ({
})
}
function flattenBy (columns, childKey) {
function flattenBy(columns, childKey) {
const flatColumns = []
const recurse = columns => {
@@ -131,7 +127,7 @@ export default function useColumns ({
}
// Build the header groups from the bottom up
function makeHeaderGroups (columns, maxDepth) {
function makeHeaderGroups(columns, maxDepth) {
const headerGroups = []
const removeChildColumns = column => {
@@ -144,7 +140,7 @@ export default function useColumns ({
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: [],
headers: []
}
const parentColumns = []
@@ -161,16 +157,24 @@ export default function useColumns ({
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_'),
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('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length
].join('_')
})
if (isFirst || latestParentColumn.originalID !== placeholderColumn.originalID) {
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}

View File

@@ -1,24 +0,0 @@
const mergeStateAndProps = (state, defaults, props) => {
Object.keys(props).forEach(key => {
if (typeof props[key] !== 'undefined') {
state[key] = props[key]
}
})
Object.keys(defaults).forEach(key => {
if (typeof state[key] === 'undefined') {
state[key] = defaults[key]
}
})
return state
}
export default function useControlledState ([userState, userSetState], defaults, props) {
const state = mergeStateAndProps(userState, defaults, props)
const setState = (updater, type) =>
userSetState(old => updater(mergeStateAndProps(old, defaults, props)), type)
return [state, setState]
}

105
src/hooks/useExpanded.js Executable file
View File

@@ -0,0 +1,105 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { getBy, getFirstDefined, setBy } from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
defaultState.expanded = {}
addActions({
toggleExpanded: '__toggleExpanded__',
useExpanded: '__useExpanded__'
})
const propTypes = {
expandedKey: PropTypes.string
}
export const useExpanded = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useExpanded')
const {
debug,
columns,
rows,
expandedKey = 'expanded',
hooks,
state: [{ expanded }, setState]
} = props
const toggleExpandedByPath = (path, set) => {
return setState(old => {
const { expanded } = old
const existing = getBy(expanded, path)
set = getFirstDefined(set, !existing)
return {
...old,
expanded: setBy(expanded, path, set)
}
}, actions.toggleExpanded)
}
hooks.row.push(row => {
const { path } = row
row.toggleExpanded = set => toggleExpandedByPath(path, set)
})
const expandedRows = useMemo(() => {
if (debug) console.info('getExpandedRows')
const expandedRows = []
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = (row, index, depth = 0, parentPath = []) => {
// Compute some final state for the row
const path = [...parentPath, index]
row.path = path
row.depth = depth
row.isExpanded =
(row.original && row.original[expandedKey]) || getBy(expanded, path)
row.cells = columns.map(column => {
const cell = {
column,
row,
state: null,
value: row.values[column.id]
}
return cell
})
expandedRows.push(row)
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row, i, depth + 1, path))
}
}
rows.forEach((row, i) => handleRow(row, i))
return expandedRows
}, [rows, expanded, columns])
const expandedDepth = findExpandedDepth(expanded)
return {
...props,
toggleExpandedByPath,
expandedDepth,
rows: expandedRows
}
}
function findExpandedDepth(obj, depth = 1) {
return Object.values(obj).reduce((prev, curr) => {
if (typeof curr === 'object') {
return Math.max(prev, findExpandedDepth(curr, depth + 1))
}
return depth
}, 0)
}

View File

@@ -1,55 +0,0 @@
import { useMemo, useEffect } from 'react'
import { getBy } from '../utils'
export default function useExpandedRows ({
debug,
expanded,
expandedKey,
columns,
rows,
setState,
actions,
}) {
return useMemo(
() => {
if (debug) console.info('getExpandedRows')
const expandedRows = []
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = (row, index, depth = 0, parentPath = []) => {
// Compute some final state for the row
const path = [...parentPath, index]
row.path = path
row.depth = depth
row.isExpanded = (row.original && row.original[expandedKey]) || getBy(expanded, path)
row.cells = columns.map(column => {
const cell = {
column,
row,
state: null,
value: row.values[column.id],
}
return cell
})
expandedRows.push(row)
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row, i, depth + 1, path))
}
}
rows.forEach((row, i) => handleRow(row, i))
return expandedRows
},
[rows, expanded, columns]
)
}

View File

@@ -1,67 +0,0 @@
import { useMemo } from 'react'
export default function useFilteredRows ({
debug,
filters,
rows,
columns,
filterFn,
manualFilters,
}) {
return useMemo(
() => {
if (manualFilters || !Object.keys(filters).length) {
return rows
}
if (debug) console.info('getFilteredRows')
// Filters top level and nested rows
const filterRows = rows => {
let filteredRows = rows
filteredRows = Object.entries(filters).reduce((filteredSoFar, [columnID, filterValue]) => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
// Don't filter hidden columns or columns that have had their filters disabled
if (!column || column.filterable === false) {
return filteredSoFar
}
const filterMethod = column.filterMethod || filterFn
// If 'filterAll' is set to true, pass the entire dataset to the filter method
if (column.filterAll) {
return filterMethod(filteredSoFar, columnID, filterValue, column)
}
return filteredSoFar.filter(row => filterMethod(row, columnID, filterValue, column))
}, rows)
// Apply the filter to any subRows
filteredRows = filteredRows.map(row => {
if (!row.subRows) {
return row
}
return {
...row,
subRows: filterRows(row.subRows),
}
})
// then filter any rows without subcolumns because it would be strange to show
filteredRows = filteredRows.filter(row => {
if (!row.subRows) {
return true
}
return row.subRows.length > 0
})
return filteredRows
}
return filterRows(rows)
},
[rows, filters, manualFilters]
)
}

161
src/hooks/useFilters.js Executable file
View File

@@ -0,0 +1,161 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { defaultFilterFn, getFirstDefined } from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
defaultState.filters = {}
addActions({
setFilter: '__setFilter__',
setAllFilters: '__setAllFilters__'
})
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
filterFn: PropTypes.func,
filterAll: PropTypes.bool,
canFilter: PropTypes.bool,
Filter: PropTypes.any
})
),
filterFn: PropTypes.func,
manualFilters: PropTypes.bool
}
export const useFilters = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFilters')
const {
debug,
rows,
columns,
filterFn = defaultFilterFn,
manualFilters,
disableFilters,
hooks,
state: [{ filters }, setState]
} = props
columns.forEach(column => {
const { id, accessor, canFilter } = column
column.canFilter = accessor
? getFirstDefined(
canFilter,
disableFilters === true ? false : undefined,
true
)
: false
// Was going to add this to the filter hook
column.filterValue = filters[id]
})
const setFilter = (id, val) => {
return setState(old => {
if (typeof val === 'undefined') {
const { [id]: prev, ...rest } = filters
return {
...old,
filters: {
...rest
}
}
}
return {
...old,
filters: {
...filters,
[id]: val
}
}
}, actions.setFilter)
}
const setAllFilters = filters => {
return setState(old => {
return {
...old,
filters
}
}, actions.setAllFilters)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canFilter) {
column.setFilter = val => setFilter(column.id, val)
}
})
return columns
})
const filteredRows = useMemo(() => {
if (manualFilters || !Object.keys(filters).length) {
return rows
}
if (debug) console.info('getFilteredRows')
// Filters top level and nested rows
const filterRows = rows => {
let filteredRows = rows
filteredRows = Object.entries(filters).reduce(
(filteredSoFar, [columnID, filterValue]) => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
// Don't filter hidden columns or columns that have had their filters disabled
if (!column || column.filterable === false) {
return filteredSoFar
}
const filterMethod = column.filterMethod || filterFn
// If 'filterAll' is set to true, pass the entire dataset to the filter method
if (column.filterAll) {
return filterMethod(filteredSoFar, columnID, filterValue, column)
}
return filteredSoFar.filter(row =>
filterMethod(row, columnID, filterValue, column)
)
},
rows
)
// Apply the filter to any subRows
filteredRows = filteredRows.map(row => {
if (!row.subRows) {
return row
}
return {
...row,
subRows: filterRows(row.subRows)
}
})
// then filter any rows without subcolumns because it would be strange to show
filteredRows = filteredRows.filter(row => {
if (!row.subRows) {
return true
}
return row.subRows.length > 0
})
return filteredRows
}
return filterRows(rows)
}, [rows, filters, manualFilters])
return {
...props,
setFilter,
setAllFilters,
rows: filteredRows
}
}

View File

@@ -1,115 +1,135 @@
import { useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import { getFirstDefined, sum } from './utils'
const propTypes = {
defaultFlex: PropTypes.number,
}
import { getFirstDefined, sum } from '../utils'
export const actions = {}
export default function useFlexLayout (api, props = {}) {
// Validate props
const propTypes = {
defaultFlex: PropTypes.number
}
export const useFlexLayout = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFlexLayout')
const {
defaultFlex = 1,
hooks: {
getRowProps, getHeaderRowProps, getHeaderProps, getCellProps,
},
visibleColumns,
} = api
columns: columnsHooks,
getRowProps,
getHeaderRowProps,
getHeaderProps,
getCellProps
}
} = props
const { defaultFlex = 1 } = props
columnsHooks.push((columns, api) => {
const visibleColumns = columns.filter(column => {
column.visible =
typeof column.show === 'function' ? column.show(api) : !!column.show
return column.visible
})
const [columnMeasurements, setColumnMeasurements] = useState({})
const columnMeasurements = {}
const rowStyles = useMemo(
() => {
let sumWidth = 0
visibleColumns.forEach(column => {
const { width, minWidth } = getSizesForColumn(
column,
defaultFlex,
undefined,
undefined,
api
)
if (width) {
sumWidth += width
} else if (minWidth) {
sumWidth += minWidth
} else {
sumWidth += defaultFlex
}
})
let sumWidth = 0
visibleColumns.forEach(column => {
const { width, minWidth } = getSizesForColumn(
column,
defaultFlex,
undefined,
undefined,
api
)
if (width) {
sumWidth += width
} else if (minWidth) {
sumWidth += minWidth
} else {
sumWidth += defaultFlex
}
})
const rowStyles = {
style: {
display: 'flex',
minWidth: `${sumWidth}px`
}
}
api.rowStyles = rowStyles
getRowProps.push(() => rowStyles)
getHeaderRowProps.push(() => rowStyles)
getHeaderProps.push(column => ({
style: {
boxSizing: 'border-box',
...getStylesForColumn(column, columnMeasurements, defaultFlex, api)
}
// [refKey]: el => {
// renderedCellInfoRef.current[key] = {
// column,
// el
// };
// },
}))
getCellProps.push(cell => {
return {
style: {
display: 'flex',
minWidth: `${sumWidth}px`,
},
display: 'block',
boxSizing: 'border-box',
...getStylesForColumn(
cell.column,
columnMeasurements,
defaultFlex,
undefined,
api
)
}
// [refKey]: el => {
// renderedCellInfoRef.current[columnPathStr] = {
// column,
// el
// };
// }
}
},
[visibleColumns]
)
})
getRowProps.push(row => rowStyles)
getHeaderRowProps.push(row => rowStyles)
return columns
})
getHeaderProps.push(column => ({
style: {
boxSizing: 'border-box',
...getStylesForColumn(column, columnMeasurements, defaultFlex, api),
},
// [refKey]: el => {
// renderedCellInfoRef.current[key] = {
// column,
// el
// };
// },
}))
getCellProps.push(cell => ({
style: {
display: 'block',
boxSizing: 'border-box',
...getStylesForColumn(cell.column, columnMeasurements, defaultFlex, undefined, api),
},
// [refKey]: el => {
// renderedCellInfoRef.current[columnPathStr] = {
// column,
// el
// };
// }
}))
return {
rowStyles,
}
return props
}
function getStylesForColumn (column, columnMeasurements, defaultFlex, api) {
const { flex, width, maxWidth } = getSizesForColumn(column, columnMeasurements, defaultFlex, api)
// Utils
function getStylesForColumn(column, columnMeasurements, defaultFlex, api) {
const { flex, width, maxWidth } = getSizesForColumn(
column,
columnMeasurements,
defaultFlex,
api
)
return {
flex: `${flex} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`,
maxWidth: `${maxWidth}px`
}
}
function getSizesForColumn (
{
columns, id, width, minWidth, maxWidth,
},
function getSizesForColumn(
{ columns, id, width, minWidth, maxWidth },
columnMeasurements,
defaultFlex,
api
) {
if (columns) {
columns = columns
.map(column => getSizesForColumn(column, columnMeasurements, defaultFlex, api))
.map(column =>
getSizesForColumn(column, columnMeasurements, defaultFlex, api)
)
.filter(Boolean)
if (!columns.length) {
@@ -123,7 +143,7 @@ function getSizesForColumn (
return {
flex,
width,
maxWidth,
maxWidth
}
}
@@ -133,7 +153,7 @@ function getSizesForColumn (
width === 'auto'
? columnMeasurements[id] || defaultFlex
: getFirstDefined(width, minWidth, defaultFlex),
maxWidth,
maxWidth
}
}

189
src/hooks/useGroupBy.js Executable file
View File

@@ -0,0 +1,189 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import * as aggregations from '../aggregations'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
import {
mergeProps,
applyPropHooks,
defaultGroupByFn,
getFirstDefined
} from '../utils'
defaultState.groupBy = []
addActions({
toggleGroupBy: '__toggleGroupBy__'
})
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
aggregate: PropTypes.func,
canGroupBy: PropTypes.bool,
Aggregated: PropTypes.any
})
),
groupByFn: PropTypes.func,
manualGrouping: PropTypes.bool,
aggregations: PropTypes.object
}
export const useGroupBy = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useGroupBy')
const {
debug,
rows,
columns,
groupByFn = defaultGroupByFn,
manualGroupBy,
disableGrouping,
aggregations: userAggregations = {},
hooks,
state: [{ groupBy }, setState]
} = props
columns.forEach(column => {
const { id, accessor, canGroupBy } = column
column.grouped = groupBy.includes(id)
column.canGroupBy = accessor
? getFirstDefined(
canGroupBy,
disableGrouping === true ? false : undefined,
true
)
: false
column.Aggregated = column.Aggregated || column.Cell
})
const toggleGroupBy = (id, toggle) => {
return setState(old => {
const resolvedToggle =
typeof set !== 'undefined' ? toggle : !groupBy.includes(id)
if (resolvedToggle) {
return {
...old,
groupBy: [...groupBy, id]
}
}
return {
...old,
groupBy: groupBy.filter(d => d !== id)
}
}, actions.toggleGroupBy)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canGroupBy) {
column.toggleGroupBy = () => toggleGroupBy(column.id)
}
})
return columns
})
hooks.getGroupByToggleProps = []
const addGroupByToggleProps = (columns, api) => {
columns.forEach(column => {
const { canGroupBy } = column
column.getGroupByToggleProps = props => {
return mergeProps(
{
onClick: canGroupBy
? e => {
e.persist()
column.toggleGroupBy()
}
: undefined,
style: {
cursor: canGroupBy ? 'pointer' : undefined
},
title: 'Toggle GroupBy'
},
applyPropHooks(api.hooks.getGroupByToggleProps, column, api),
props
)
}
})
return columns
}
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) {
return rows
}
// 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
}
return row
}
)
return groupedRows
}
// Assign the new data
return groupRecursively(rows, groupBy)
}, [rows, groupBy, columns, manualGroupBy])
return {
...props,
rows: groupedRows
}
}

View File

@@ -1,77 +0,0 @@
import { useMemo } from 'react'
import * as aggregations from '../aggregations'
export default function useGroupedRows ({
debug,
groupBy,
rows,
columns,
groupByFn,
manualGroupBy,
userAggregations,
}) {
return 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])
const 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) {
return rows
}
// Group the rows together for this level
const 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,
}
return row
}
)
return groupedRows
}
// Assign the new data
return groupRecursively(rows, groupBy)
},
[rows, groupBy, columns, manualGroupBy]
)
}

129
src/hooks/usePagination.js Executable file
View File

@@ -0,0 +1,129 @@
import { useMemo, useLayoutEffect } from 'react'
import PropTypes from 'prop-types'
//
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
defaultState.pageSize = 10
defaultState.pageIndex = 0
addActions({
pageChange: '__pageChange__'
})
const propTypes = {
// General
manualPagination: PropTypes.bool
}
export const usePagination = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'usePagination')
const {
rows,
manualPagination,
debug,
state: [
{
pageSize,
pageIndex,
pageCount: userPageCount,
filters,
groupBy,
sortBy
},
setState
]
} = props
useLayoutEffect(() => {
setState(
old => ({
...old,
pageIndex: 0
}),
actions.pageChange
)
}, [filters, groupBy, sortBy])
const { pages, pageCount } = useMemo(() => {
if (manualPagination) {
return {
pages: [rows],
pageCount: userPageCount
}
}
if (debug) console.info('getPages')
// Create a new pages with the first page ready to go.
const pages = rows.length ? [] : [[]]
// Start the pageIndex and currentPage cursors
let cursor = 0
while (cursor < rows.length) {
const end = cursor + pageSize
pages.push(rows.slice(cursor, end))
cursor = end
}
const pageCount = pages.length
return {
pages,
pageCount,
pageOptions
}
}, [rows, pageSize, userPageCount])
const pageOptions = [...new Array(pageCount)].map((d, i) => i)
const page = manualPagination ? rows : pages[pageIndex] || []
const canPreviousPage = pageIndex > 0
const canNextPage = pageIndex < pageCount - 1
const gotoPage = pageIndex => {
if (debug) console.info('gotoPage')
return setState(old => {
if (pageIndex < 0 || pageIndex > pageCount - 1) {
return old
}
return {
...old,
pageIndex
}
}, actions.pageChange)
}
const previousPage = () => {
return gotoPage(pageIndex - 1)
}
const nextPage = () => {
return gotoPage(pageIndex + 1)
}
const setPageSize = pageSize => {
setState(old => {
const topRowIndex = old.pageSize * old.pageIndex
const pageIndex = Math.floor(topRowIndex / pageSize)
return {
...old,
pageIndex,
pageSize
}
}, actions.setPageSize)
}
return {
...props,
pages,
pageOptions,
page,
canPreviousPage,
canNextPage,
gotoPage,
previousPage,
nextPage,
setPageSize
}
}

52
src/hooks/useRows.js Executable file
View File

@@ -0,0 +1,52 @@
import { useMemo } 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 = 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,
subRows,
depth
}
// 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))
}, [data, columns])
return {
...props,
rows: accessedRows
}
}

256
src/hooks/useSortBy.js Executable file
View File

@@ -0,0 +1,256 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
import {
mergeProps,
applyPropHooks,
getFirstDefined,
defaultOrderByFn,
defaultSortByFn
} from '../utils'
defaultState.sortBy = []
addActions({
sortByChange: '__sortByChange__'
})
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
sortByFn: PropTypes.func,
efaultSortDesc: PropTypes.bool
})
),
sortByFn: PropTypes.func,
manualSorting: PropTypes.bool,
disableSorting: PropTypes.bool,
defaultSortDesc: PropTypes.bool,
disableMultiSort: PropTypes.bool
}
export const useSortBy = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useSortBy')
const {
debug,
rows,
columns,
orderByFn = defaultOrderByFn,
sortByFn = defaultSortByFn,
manualSorting,
disableSorting,
defaultSortDesc,
hooks,
state: [{ sortBy }, setState]
} = props
columns.forEach(column => {
const { accessor, canSortBy } = column
column.canSortBy = accessor
? getFirstDefined(
canSortBy,
disableSorting === true ? false : undefined,
true
)
: false
})
// Updates sorting based on a columnID, desc flag and multi flag
const toggleSortByID = (columnID, desc, multi) => {
return setState(old => {
const { sortBy } = old
// Find the column for this columnID
const column = columns.find(d => d.id === columnID)
const resolvedDefaultSortDesc = getFirstDefined(
column.defaultSortDesc,
defaultSortDesc
)
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnID)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
// What should we do with this filter?
let action
if (!multi) {
if (sortBy.length <= 1 && existingSortBy) {
if (existingSortBy.desc) {
action = 'remove'
} else {
action = 'toggle'
}
} else {
action = 'replace'
}
} else {
if (!existingSortBy) {
action = 'add'
} else {
if (hasDescDefined) {
action = 'set'
} else {
action = 'toggle'
}
}
}
if (action === 'replace') {
newSortBy = [
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc
}
]
} else if (action === 'add') {
newSortBy = [
...sortBy,
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc
}
]
} else if (action === 'set') {
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc
}
}
return d
})
} else if (action === 'toggle') {
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc: !existingSortBy.desc
}
}
return d
})
} else if (action === 'remove') {
newSortBy = []
}
return {
...old,
sortBy: newSortBy
}
}, actions.sortByChange)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canSortBy) {
column.toggleSortBy = (desc, multi) =>
toggleSortByID(column.id, desc, multi)
}
})
return columns
})
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
)
}
})
return columns
}
hooks.columns.push(addSortByToggleProps)
hooks.headers.push(addSortByToggleProps)
// 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(() => {
if (manualSorting || !sortBy.length) {
return rows
}
if (debug) console.info('getSortedRows')
const sortMethodsByColumnID = {}
columns
.filter(col => col.sortMethod)
.forEach(col => {
sortMethodsByColumnID[col.id] = col.sortMethod
})
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
// This will also perform a stable sorting using the row index
// if needed.
const sortedData = orderByFn(
rows,
sortBy.map(sort => {
// Support custom sorting methods for each column
const columnSortBy = sortMethodsByColumnID[sort.id]
// Return the correct sortFn
return (a, b) =>
(columnSortBy || sortByFn)(
a.values[sort.id],
b.values[sort.id],
sort.desc
)
}),
// Map the directions
sortBy.map(d => !d.desc)
)
// TODO: this should be optimized. Not good to loop again
sortedData.forEach(row => {
if (!row.subRows) {
return
}
row.subRows = sortData(row.subRows)
})
return sortedData
}
return sortData(rows)
}, [rows, columns, sortBy, manualSorting])
return {
...props,
rows: sortedRows
}
}

View File

@@ -1,60 +0,0 @@
import { useMemo } from 'react'
export default function useSortedRows ({
debug,
sortBy,
rows,
columns,
orderByFn,
sortByFn,
manualSorting,
}) {
return useMemo(
() => {
if (manualSorting || !sortBy.length) {
return rows
}
if (debug) console.info('getSortedRows')
const sortMethodsByColumnID = {}
columns
.filter(col => col.sortMethod)
.forEach(col => {
sortMethodsByColumnID[col.id] = col.sortMethod
})
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
// This will also perform a stable sorting using the row index
// if needed.
const sortedData = orderByFn(
rows,
sortBy.map(sort => {
// Support custom sorting methods for each column
const columnSortBy = sortMethodsByColumnID[sort.id]
// Return the correct sortFn
return (a, b) =>
(columnSortBy || sortByFn)(a.values[sort.id], b.values[sort.id], sort.desc)
}),
// Map the directions
sortBy.map(d => !d.desc)
)
// TODO: this should be optimized. Not good to loop again
sortedData.forEach(row => {
if (!row.subRows) {
return
}
row.subRows = sortData(row.subRows)
})
return sortedData
}
return sortData(rows)
},
[rows, columns, sortBy, manualSorting]
)
}

192
src/hooks/useTable.js Executable file
View File

@@ -0,0 +1,192 @@
import PropTypes from 'prop-types'
//
import { flexRender, applyHooks, applyPropHooks, mergeProps } from '../utils'
import { useTableState } from './useTableState'
const renderErr =
'You must specify a render "type". This could be "Header", "Filter", or any other custom renderers you have set on your column.'
const propTypes = {
// General
data: PropTypes.array.isRequired,
debug: PropTypes.bool
}
export const useTable = (props, ...plugins) => {
// Validate props
PropTypes.checkPropTypes(propTypes, props, 'property', 'useTable')
// Destructure props
let { data = [], state: userState, debug } = props
debug = process.env.NODE_ENV === 'production' ? false : debug
// Always provide a default state
const defaultState = useTableState()
// 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: [],
renderableRows: [],
getTableProps: [],
getRowProps: [],
getHeaderRowProps: [],
getHeaderProps: [],
getCellProps: []
}
// The initial api
let api = {
...props,
data,
state,
hooks
}
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')
// Run the beforeRender hook
if (debug) console.time('hooks.beforeRender')
applyHooks(api.hooks.beforeRender, undefined, api)
if (debug) console.timeEnd('hooks.beforeRender')
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.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 = {}) => {
if (!type) {
throw new Error(renderErr)
}
return flexRender(column[type], {
...api,
...column,
...userProps
})
}
// Give columns/headers getHeaderProps
column.getHeaderProps = props =>
mergeProps(
{
key: ['header', column.id].join('_')
},
applyPropHooks(api.hooks.getHeaderProps, column, api),
props
)
})
if (debug) console.time('hooks.headerGroups')
api.headerGroups = applyHooks(
api.hooks.headerGroups,
api.headerGroups,
api
).filter((headerGroup, i) => {
// Filter out any headers and headerGroups that don't have visible columns
headerGroup.headers = headerGroup.headers.filter(header => {
const recurse = columns =>
columns.filter(column => {
if (column.columns) {
return recurse(column.columns)
}
return column.visible
}).length
if (header.columns) {
return recurse(header.columns)
}
return header.visible
})
// Give headerGroups getRowProps
if (headerGroup.headers.length) {
headerGroup.getRowProps = (props = {}) =>
mergeProps(
{
key: [`header${i}`].join('_')
},
applyPropHooks(api.hooks.getHeaderRowProps, headerGroup, api),
props
)
return true
}
return false
})
if (debug) console.timeEnd('hooks.headerGroups')
// 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')
// This function is absolutely necessary and MUST be called on
// any rows the user wishes to be displayed.
api.prepareRow = row => {
const { index } = row
row.getRowProps = props =>
mergeProps(
{ key: ['row', index].join('_') },
applyHooks(api.hooks.getRowProps, row, api),
props
)
row.cells = row.cells.filter(cell => cell.column.visible)
row.cells.forEach(cell => {
if (!cell) {
return
}
const { column } = cell
cell.getCellProps = props => {
const columnPathStr = [index, column.id].join('_')
return mergeProps(
{
key: ['cell', columnPathStr].join('_')
},
applyPropHooks(api.hooks.getCellProps, cell, api),
props
)
}
cell.render = (type, userProps = {}) => {
if (!type) {
throw new Error(
'You must specify a render "type". This could be "Cell", "Header", "Filter", "Aggregated" or any other custom renderers you have set on your column.'
)
}
return flexRender(column[type], {
...api,
...cell,
...userProps
})
}
})
}
api.getTableProps = userProps =>
mergeProps(applyPropHooks(api.hooks.getTableProps, api), userProps)
api.getRowProps = userProps =>
mergeProps(applyPropHooks(api.hooks.getRowProps, api), userProps)
return api
}

View File

@@ -1,156 +0,0 @@
import { getBy, setBy, getFirstDefined } from '../utils'
export default function useTableMethods ({
debug,
setState,
actions,
groupBy,
columns,
defaultSortDesc,
filters,
}) {
const toggleExpandedByPath = (path, set) =>
setState(old => {
const { expanded } = old
const existing = getBy(expanded, path)
set = getFirstDefined(set, !existing)
return {
...old,
expanded: setBy(expanded, path, set),
}
}, actions.toggleExpanded)
// Updates sorting based on a columnID, desc flag and multi flag
const toggleSortByID = (columnID, desc, multi) =>
setState(old => {
const { sortBy } = old
// Find the column for this columnID
const column = columns.find(d => d.id === columnID)
const resolvedDefaultSortDesc = getFirstDefined(column.defaultSortDesc, defaultSortDesc)
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnID)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
// What should we do with this filter?
let action
if (!multi) {
if (sortBy.length <= 1 && existingSortBy) {
if (existingSortBy.desc) {
action = 'remove'
} else {
action = 'toggle'
}
} else {
action = 'replace'
}
} else if (!existingSortBy) {
action = 'add'
} else if (hasDescDefined) {
action = 'set'
} else {
action = 'toggle'
}
if (action === 'replace') {
newSortBy = [
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc,
},
]
} else if (action === 'add') {
newSortBy = [
...sortBy,
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc,
},
]
} else if (action === 'set') {
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc,
}
}
return d
})
} else if (action === 'toggle') {
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc: !existingSortBy.desc,
}
}
return d
})
} else if (action === 'remove') {
newSortBy = []
}
return {
...old,
sortBy: newSortBy,
}
}, actions.sortByChange)
const toggleGroupBy = (id, toggle) =>
setState(old => {
const resolvedToggle = typeof set !== 'undefined' ? toggle : !groupBy.includes(id)
if (resolvedToggle) {
return {
...old,
groupBy: [...groupBy, id],
}
}
return {
...old,
groupBy: groupBy.filter(d => d !== id),
}
}, actions.toggleGroupBy)
const setFilter = (id, val) =>
setState(old => {
if (typeof val === 'undefined') {
const { [id]: prev, ...rest } = filters
return {
...old,
filters: {
...rest,
},
}
}
return {
...old,
filters: {
...filters,
[id]: val,
},
}
}, actions.setFilter)
const setAllFilters = filters =>
setState(
old => ({
...old,
filters,
}),
actions.setAllFilters
)
return {
toggleExpandedByPath,
toggleSortByID,
toggleGroupBy,
setFilter,
setAllFilters,
}
}

34
src/hooks/useTableState.js Executable file
View File

@@ -0,0 +1,34 @@
import { useState, useMemo } from 'react'
export const defaultState = {}
const defaultReducer = (old, newState) => newState
export const useTableState = (
initialState = {},
overrides = {},
{ reducer = defaultReducer, useState: userUseState = useState } = {}
) => {
let [state, setState] = userUseState({
...defaultState,
...initialState
})
const overriddenState = useMemo(() => {
const newState = {
...state
}
Object.keys(overrides).forEach(key => {
newState[key] = overrides[key]
})
return newState
}, [state, ...Object.values(overrides)])
const reducedSetState = (updater, type) =>
setState(old => {
const newState = updater(old)
return reducer(old, newState, type)
})
return [overriddenState, reducedSetState]
}

View File

@@ -0,0 +1,51 @@
import { useState } from 'react'
// Token pagination behaves a bit differently from
// index based pagination. This hook aids in that process.
export const useTokenPagination = () => {
const [pageToken, setPageToken] = useState()
const [nextPageToken, setNextPageToken] = useState()
const [previousPageTokens, setPreviousPageTokens] = useState([])
const [pageIndex, setPageIndex] = useState(0)
// Since we're using pagination tokens intead of index, we need
// to be a bit clever with page-like navigation here.
const nextPage = () => {
setPageIndex(old => old + 1)
setPreviousPageTokens(old => [...old, pageToken])
setPageToken(nextPageToken)
}
const previousPage = () => {
setPageIndex(old => old - 1)
setPreviousPageTokens(old =>
[...old]
.reverse()
.slice(1)
.reverse()
)
setPageToken(previousPageTokens[previousPageTokens.length - 1])
}
const resetPagination = () => {
setPageToken(undefined)
setPageIndex(0)
setNextPageToken(undefined)
setPreviousPageTokens([])
}
const canPreviousPage = previousPageTokens.length
const canNextPage = nextPageToken
return {
setNextPageToken,
pageToken,
pageIndex,
previousPage,
nextPage,
canPreviousPage,
canNextPage,
resetPagination
}
}

View File

@@ -1,11 +1,12 @@
import useReactTable, { actions as mainActions } from './useReactTable'
import usePagination, { actions as paginationActions } from './usePagination'
import useFlexLayout, { actions as flexLayoutActions } from './useFlexLayout'
const actions = {
...mainActions,
...paginationActions,
...flexLayoutActions,
}
export { useReactTable, usePagination, useFlexLayout, actions }
export { useTable } from './hooks/useTable'
export { useColumns } from './hooks/useColumns'
export { useRows } from './hooks/useRows'
export { useExpanded } from './hooks/useExpanded'
export { useFilters } from './hooks/useFilters'
export { useGroupBy } from './hooks/useGroupBy'
export { useSortBy } from './hooks/useSortBy'
export { usePagination } from './hooks/usePagination'
export { useTableState } from './hooks/useTableState'
export { useFlexLayout } from './hooks/useFlexLayout'
export { useTokenPagination } from './hooks/useTokenPagination'
export { actions } from './actions'

View File

@@ -1,155 +0,0 @@
import { useMemo, useState, useLayoutEffect } from 'react'
import PropTypes from 'prop-types'
import { warnUnknownProps } from './utils'
import useControlledState from './hooks/useControlledState'
const propTypes = {
defaultPageSize: PropTypes.number,
defaultPageIndex: PropTypes.number,
pageSize: PropTypes.number,
pages: PropTypes.number,
pageIndex: PropTypes.number,
onStateChange: PropTypes.func,
stateReducer: PropTypes.func,
subRowsKey: PropTypes.string,
debug: PropTypes.bool,
}
export const actions = {
pageChange: '__pageChange__',
}
export default function usePagination (
{
debug: parentDebug, rows, prepareRows, groupedRows, sortedRows, filteredRows,
},
props = {}
) {
// Validate props
PropTypes.checkPropTypes(propTypes, props, 'property', 'usePagination')
// Destructure props
const {
defaultPageSize = 10,
defaultPageIndex = 0,
pageSize: userPageSize,
pageIndex: userPageIndex,
state: userState,
manualPagination,
pageCount: userPageCount,
pageOptions: userPageOptions,
debug = parentDebug,
...rest
} = props
warnUnknownProps(rest)
const defaultState = useState({})
const localState = userState || defaultState
// Build the controllable state
const [{ pageSize, pageIndex }, setState] = useControlledState(
localState,
{
pageSize: defaultPageSize,
pageIndex: defaultPageIndex,
},
{
pageSize: userPageSize,
pageIndex: userPageIndex,
}
)
useLayoutEffect(
() => {
if (manualPagination) {
return
}
setState(
old => ({
...old,
pageIndex: 0,
}),
actions.pageChange
)
},
[groupedRows, sortedRows, filteredRows]
)
const { pages, pageCount } = useMemo(
() => {
if (manualPagination) {
return {
pages: [rows],
pageCount: userPageCount,
}
}
if (debug) console.info('getPages')
// Create a new pages with the first page ready to go.
const pages = rows.length ? [] : [[]]
// Start the pageIndex and currentPage cursors
let cursor = 0
while (cursor < rows.length) {
const end = cursor + pageSize
pages.push(rows.slice(cursor, end))
cursor = end
}
const pageCount = pages.length
return {
pages,
pageCount,
pageOptions,
}
},
[rows, pageSize, userPageCount]
)
const pageOptions = [...new Array(pageCount)].map((d, i) => i)
const page = manualPagination ? rows : pages[pageIndex] || []
const canPreviousPage = pageIndex > 0
const canNextPage = pageIndex < pageCount - 1
const gotoPage = pageIndex => {
if (debug) console.info('gotoPage')
return setState(old => {
if (pageIndex < 0 || pageIndex > pageCount - 1) {
return old
}
return {
...old,
pageIndex,
}
}, actions.pageChange)
}
const previousPage = () => gotoPage(pageIndex - 1)
const nextPage = () => gotoPage(pageIndex + 1)
const setPageSize = pageSize => {
setState(old => ({
...old,
pageSize,
}))
}
prepareRows(page)
return {
pages,
pageIndex,
pageOptions,
page,
canPreviousPage,
canNextPage,
gotoPage,
previousPage,
nextPage,
setPageSize,
}
}

View File

@@ -1,470 +0,0 @@
import { useState, useMemo, useRef } from 'react'
import PropTypes from 'prop-types'
//
import {
defaultOrderByFn,
defaultSortByFn,
defaultGroupByFn,
defaultFilterFn,
flexRender,
findExpandedDepth,
applyHooks,
mergeProps,
warnUnknownProps,
} from './utils'
// Internal Hooks
import useControlledState from './hooks/useControlledState'
import useColumns from './hooks/useColumns'
import useTableMethods from './hooks/useTableMethods'
import useAccessedRows from './hooks/useAccessedRows'
import useGroupedRows from './hooks/useGroupedRows'
import useFilteredRows from './hooks/useFilteredRows'
import useSortedRows from './hooks/useSortedRows'
import useExpandedRows from './hooks/useExpandedRows'
const defaultFlex = 1
const renderErr =
'You must specify a render "type". This could be "Header", "Filter", or any other custom renderers you have set on your column.'
export const actions = {
sortByChange: '__sortByChange',
updateAutoWidth: '__updateAutoWidth__',
toggleGroupBy: '__toggleGroupBy__',
toggleExpanded: '__toggleExpanded__',
setExpanded: '__setExpanded__',
setFilter: '__setFilter__',
setAllFilters: '__setAllFilters__',
}
const propTypes = {
// General
data: PropTypes.any,
columns: PropTypes.arrayOf(
PropTypes.shape({
aggregate: PropTypes.func,
filterFn: PropTypes.func,
filterAll: PropTypes.bool,
sortByFn: PropTypes.func,
resolvedDefaultSortDesc: PropTypes.bool,
canSortBy: PropTypes.bool,
canGroupBy: PropTypes.bool,
Cell: PropTypes.any,
Header: PropTypes.any,
Filter: PropTypes.any,
})
),
defaultFilters: PropTypes.array,
defaultSortBy: PropTypes.array,
defaultGroupBy: PropTypes.array,
groupBy: PropTypes.array,
filters: PropTypes.array,
sortBy: PropTypes.array,
filterFn: PropTypes.func,
sortByFn: PropTypes.func,
orderByFn: PropTypes.func,
groupByFn: PropTypes.func,
manualGrouping: PropTypes.bool,
manualFilters: PropTypes.bool,
manualSorting: PropTypes.bool,
disableGrouping: PropTypes.bool,
disableFilters: PropTypes.bool,
disableSorting: PropTypes.bool,
defaultSortDesc: PropTypes.bool,
disableMultiSort: PropTypes.bool,
subRowsKey: PropTypes.string,
expandedKey: PropTypes.string,
userAggregations: PropTypes.object,
debug: PropTypes.bool,
}
export default function useReactTable (props) {
// Validate props
PropTypes.checkPropTypes(propTypes, props, 'property', 'useReactTable')
// Destructure props
const {
data = [],
state: userState,
columns: userColumns,
sortBy: userSortBy,
groupBy: userGroupBy,
filters: userFilters,
expanded: userExpanded,
defaultSortBy = [],
defaultGroupBy = [],
defaultFilters = {},
defaultExpanded = {},
manualGrouping,
manualFilters,
manualSorting,
disableSorting,
disableGrouping,
disableFilters,
defaultSortDesc,
disableMultiSort,
subRowsKey = 'subRows',
expandedKey = 'expanded',
filterFn = defaultFilterFn,
sortByFn = defaultSortByFn,
orderByFn = defaultOrderByFn,
groupByFn = defaultGroupByFn,
userAggregations = {},
debug,
...rest
} = props
warnUnknownProps(rest)
const defaultState = useState({})
const localState = userState || defaultState
// Build the controllable state
const [{
sortBy, groupBy, filters, expanded,
}, setState] = useControlledState(
localState,
{
sortBy: defaultSortBy,
groupBy: defaultGroupBy,
filters: defaultFilters,
expanded: defaultExpanded,
},
{
sortBy: userSortBy,
groupBy: userGroupBy,
filters: userFilters,
expanded: userExpanded,
}
)
const expandedDepth = findExpandedDepth(expanded)
const renderedCellInfoRef = useRef({})
// Build the base column
let { columns, headerGroups, headers } = useColumns({
debug,
groupBy,
userColumns,
defaultFlex,
renderedCellInfoRef,
disableSorting,
disableGrouping,
disableFilters,
})
// Use data and columns as memoization
const accessedRows = useAccessedRows({
debug,
data,
columns,
subRowsKey,
})
// Use rows and groupBy as memoization for row grouping
const groupedRows = useGroupedRows({
debug,
rows: accessedRows,
groupBy,
columns,
groupByFn,
manualGrouping,
userAggregations,
})
const filteredRows = useFilteredRows({
debug,
rows: groupedRows,
filters,
columns,
filterFn,
manualFilters,
})
const sortedRows = useSortedRows({
debug,
rows: filteredRows,
sortBy,
columns,
orderByFn,
sortByFn,
manualSorting,
})
const rows = useExpandedRows({
debug,
rows: sortedRows,
expanded,
expandedKey,
columns,
groupBy,
setState,
actions,
})
// 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
column.filterVal = filters[id]
})
// Public API
const {
toggleExpandedByPath,
toggleSortByID,
toggleGroupBy,
setFilter,
setAllFilters,
} = useTableMethods({
debug,
setState,
actions,
groupBy,
columns,
defaultSortDesc,
filters,
})
const state = {
// State
columns,
expandedDepth,
accessedRows,
groupedRows,
filteredRows,
sortedRows,
rows,
headerGroups,
renderedCellInfoRef,
disableMultiSort,
// Controllable state
groupBy,
filters,
sortBy,
expanded,
}
const api = {
// State manager,
...state,
// Methods
toggleExpandedByPath,
toggleSortByID,
toggleGroupBy,
setFilter,
setAllFilters,
}
columns.forEach(column => attachApi(column, api))
headers.forEach(header => attachApi(header, api))
const visibleColumns = columns.filter(column => {
column.visible = typeof column.show === 'function' ? column.show(state) : !!column.show
return column.visible
})
state.visibleColumns = visibleColumns
api.visibleColumns = visibleColumns
api.hooks = {
getTableProps: [],
getRowProps: [],
getHeaderRowProps: [],
getCellProps: [],
getHeaderProps: [],
getSortByToggleProps: [],
getGroupByToggleProps: [],
}
api.getTableProps = props => mergeProps(
{
style: {
overflowX: 'auto',
},
},
applyHooks(api.hooks.getTableProps),
props
)
api.getRowProps = props => mergeProps(applyHooks(api.hooks.getRowProps), props)
// Filter out hidden header groups or header groups that
// have no visible columns, then add the getRowProps method
headerGroups = useMemo(
() =>
headerGroups.filter((headerGroup, i) => {
headerGroup.headers = headerGroup.headers.filter(header => {
const recurse = columns => columns.filter(column => {
if (column.columns) {
return recurse(column.columns)
}
return column.visible
}).length
if (header.columns) {
return recurse(header.columns)
}
return header.visible
})
if (headerGroup.headers.length) {
headerGroup.getRowProps = (props = {}) => mergeProps(
{
key: [`header${i}`].join('_'),
},
applyHooks(api.hooks.getHeaderRowProps, headerGroup),
props
)
return true
}
return false
}),
[headerGroups]
)
// A function that mutates and prepares page rows with methods
api.prepareRows = rows => {
rows.forEach((row, i) => {
const { path } = row
row.getRowProps = props => mergeProps(
{ key: ['row', i].join('_') },
applyHooks(api.hooks.getRowProps, row),
props
)
row.toggleExpanded = set => toggleExpandedByPath(path, set)
row.cells = row.cells.filter(cell => cell.column.visible);
[row.groupByCell, ...row.cells].forEach(cell => {
if (!cell) {
return
}
const { column } = cell
cell.getCellProps = props => {
const columnPathStr = [i, column.id].join('_')
return mergeProps(
{
key: ['cell', columnPathStr].join('_'),
},
applyHooks(api.hooks.getCellProps, cell),
props
)
}
cell.render = (type, userProps = {}) => {
if (!type) {
throw new Error(
'You must specify a render "type". This could be "Cell", "Header", "Filter", "Aggregated" or any other custom renderers you have set on your column.'
)
}
return flexRender(column[type], {
...api,
...cell,
...userProps,
})
}
})
})
}
const makeHook = api => {
api.hook = (hook, ...args) => {
const result = hook(api, ...args) || {}
const newApi = {
...api,
...result,
}
makeHook(newApi)
return newApi
}
}
makeHook(api)
return api
}
function attachApi (column, api) {
const {
id, canSortBy, canGroupBy, canFilter,
} = column
const { toggleSortByID, toggleGroupBy, setFilter } = api
column.render = (type, userProps = {}) => {
if (!type) {
throw new Error(renderErr)
}
return flexRender(column[type], {
...api,
...column,
...userProps,
})
}
if (canSortBy) {
column.toggleSortBy = (desc, multi) => toggleSortByID(id, desc, multi)
}
if (canGroupBy) {
column.toggleGroupBy = () => toggleGroupBy(id)
}
if (canFilter) {
column.setFilter = val => setFilter(id, val)
}
column.getSortByToggleProps = props => mergeProps(
{
onClick: canSortBy
? e => {
e.persist()
column.toggleSortBy(undefined, !api.disableMultiSort && e.shiftKey)
}
: undefined,
style: {
cursor: canSortBy ? 'pointer' : undefined,
},
title: 'Toggle SortBy',
},
applyHooks(api.hooks.getSortByToggleProps, column),
props
)
column.getGroupByToggleProps = props => mergeProps(
{
onClick: canGroupBy
? e => {
e.persist()
column.toggleGroupBy()
}
: undefined,
style: {
cursor: canGroupBy ? 'pointer' : undefined,
},
title: 'Toggle GroupBy',
},
applyHooks(api.hooks.getGroupByToggleProps, column),
props
)
column.getHeaderProps = props => mergeProps(
{
key: ['header', column.id].join('_'),
},
applyHooks(api.hooks.getHeaderProps, column),
props
)
}

View File

@@ -1,6 +1,6 @@
import React from 'react'
export function getBy (obj, path, def) {
export function getBy(obj, path, def) {
if (!path) {
return obj
}
@@ -14,7 +14,7 @@ export function getBy (obj, path, def) {
return typeof val !== 'undefined' ? val : def
}
export function defaultOrderByFn (arr, funcs, dirs) {
export function defaultOrderByFn(arr, funcs, dirs) {
return [...arr].sort((rowA, rowB) => {
for (let i = 0; i < funcs.length; i += 1) {
const sortFn = funcs[i]
@@ -28,7 +28,7 @@ export function defaultOrderByFn (arr, funcs, dirs) {
})
}
export function defaultSortByFn (a, b, desc) {
export function defaultSortByFn(a, b, desc) {
// force null and undefined to the bottom
a = a === null || a === undefined ? '' : a
b = b === null || b === undefined ? '' : b
@@ -47,7 +47,7 @@ export function defaultSortByFn (a, b, desc) {
return 0
}
export function getFirstDefined (...args) {
export function getFirstDefined(...args) {
for (let i = 0; i < args.length; i += 1) {
if (typeof args[i] !== 'undefined') {
return args[i]
@@ -55,16 +55,19 @@ export function getFirstDefined (...args) {
}
}
export function defaultGroupByFn (rows, grouper) {
export function defaultGroupByFn(rows, grouper) {
return rows.reduce((prev, row, i) => {
const resKey = typeof grouper === 'function' ? grouper(row.values, i) : row.values[grouper]
const resKey =
typeof grouper === 'function'
? grouper(row.values, i)
: row.values[grouper]
prev[resKey] = Array.isArray(prev[resKey]) ? prev[resKey] : []
prev[resKey].push(row)
return prev
}, {})
}
export function defaultFilterFn (row, id, value, column) {
export function defaultFilterFn(row, id, value, column) {
return row.values[id] !== undefined
? String(row.values[id])
.toLowerCase()
@@ -72,59 +75,57 @@ export function defaultFilterFn (row, id, value, column) {
: true
}
export function setBy (obj = {}, path, value) {
export function setBy(obj = {}, path, value) {
const recurse = (obj, depth = 0) => {
const key = path[depth]
const target = typeof obj[key] !== 'object' ? {} : obj[key]
const subValue = depth === path.length - 1 ? value : recurse(target, depth + 1)
const subValue =
depth === path.length - 1 ? value : recurse(target, depth + 1)
return {
...obj,
[key]: subValue,
[key]: subValue
}
}
return recurse(obj)
}
export function getElementDimensions (element) {
export function getElementDimensions(element) {
const rect = element.getBoundingClientRect()
const style = window.getComputedStyle(element)
const margins = {
left: parseInt(style.marginLeft),
right: parseInt(style.marginRight),
right: parseInt(style.marginRight)
}
const padding = {
left: parseInt(style.paddingLeft),
right: parseInt(style.paddingRight),
right: parseInt(style.paddingRight)
}
return {
left: Math.ceil(rect.left),
width: Math.ceil(rect.width),
outerWidth: Math.ceil(rect.width + margins.left + margins.right + padding.left + padding.right),
outerWidth: Math.ceil(
rect.width + margins.left + margins.right + padding.left + padding.right
),
marginLeft: margins.left,
marginRight: margins.right,
paddingLeft: padding.left,
paddingRight: padding.right,
scrollWidth: element.scrollWidth,
scrollWidth: element.scrollWidth
}
}
export function flexRender (Comp, props) {
export function flexRender(Comp, props) {
if (typeof Comp === 'function') {
return Object.getPrototypeOf(Comp).isReactComponent ? <Comp {...props} /> : Comp(props)
return Object.getPrototypeOf(Comp).isReactComponent ? (
<Comp {...props} />
) : (
Comp(props)
)
}
return Comp
}
export function findExpandedDepth (obj, depth = 1) {
return Object.values(obj).reduce((prev, curr) => {
if (typeof curr === 'object') {
return Math.max(prev, findExpandedDepth(curr, depth + 1))
}
return depth
}, 0)
}
export const mergeProps = (...groups) => {
let props = {}
groups.forEach(({ style = {}, className, ...rest } = {}) => {
@@ -133,15 +134,18 @@ export const mergeProps = (...groups) => {
...rest,
style: {
...(props.style || {}),
...style,
...style
},
className: [props.className, className].filter(Boolean).join(' '),
className: [props.className, className].filter(Boolean).join(' ')
}
})
return props
}
export const applyHooks = (hooks, ...args) =>
export const applyHooks = (hooks, initial, ...args) =>
hooks.reduce((prev, next) => next(prev, ...args), initial)
export const applyPropHooks = (hooks, ...args) =>
hooks.reduce((prev, next) => mergeProps(prev, next(...args)), {})
export const warnUnknownProps = props => {
@@ -154,11 +158,11 @@ ${JSON.stringify(props, null, 2)}`
}
}
export function sum (arr) {
export function sum(arr) {
return arr.reduce((prev, curr) => prev + curr, 0)
}
function makePathArray (obj) {
function makePathArray(obj) {
return flattenDeep(obj)
.join('.')
.replace(/\[/g, '.')
@@ -166,7 +170,7 @@ function makePathArray (obj) {
.split('.')
}
function flattenDeep (arr, newArr = []) {
function flattenDeep(arr, newArr = []) {
if (!Array.isArray(arr)) {
newArr.push(arr)
} else {

4864
yarn.lock

File diff suppressed because it is too large Load Diff