mirror of
https://github.com/gosticks/react-table.git
synced 2025-10-16 11:55:36 +00:00
Width options (`width`, `minWidth`, `maxWidth`) options are now a part of the core column object. useBlockLayout and useAbsoluteLayout hooks now use this new internalized information to implement their layouts. Those examples have been updated. A virtualized-rows example has also been added to show off how the useBlockLayout hook can be used to virtualize rows with react-window.
424 lines
11 KiB
JavaScript
Executable File
424 lines
11 KiB
JavaScript
Executable File
import React from 'react'
|
|
import PropTypes from 'prop-types'
|
|
//
|
|
import {
|
|
applyHooks,
|
|
applyPropHooks,
|
|
mergeProps,
|
|
flexRender,
|
|
decorateColumnTree,
|
|
makeHeaderGroups,
|
|
flattenBy,
|
|
determineHeaderVisibility,
|
|
} from '../utils'
|
|
|
|
import { useTableState } from './useTableState'
|
|
|
|
const propTypes = {
|
|
// General
|
|
data: PropTypes.array.isRequired,
|
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
defaultColumn: PropTypes.object,
|
|
getSubRows: PropTypes.func,
|
|
getRowID: PropTypes.func,
|
|
debug: PropTypes.bool,
|
|
}
|
|
|
|
const renderErr =
|
|
'You must specify a valid render component. This could be "column.Cell", "column.Header", "column.Filter", "column.Aggregated" or any other custom renderer component.'
|
|
|
|
const defaultColumnInstance = {}
|
|
|
|
const defaultGetSubRows = (row, index) => row.subRows || []
|
|
const defaultGetRowID = (row, index) => index
|
|
|
|
export const useTable = (props, ...plugins) => {
|
|
// Validate props
|
|
PropTypes.checkPropTypes(propTypes, props, 'property', 'useTable')
|
|
|
|
// Destructure props
|
|
let {
|
|
data,
|
|
state: userState,
|
|
columns: userColumns,
|
|
defaultColumn = defaultColumnInstance,
|
|
getSubRows = defaultGetSubRows,
|
|
getRowID = defaultGetRowID,
|
|
debug,
|
|
} = props
|
|
|
|
debug = process.env.NODE_ENV === 'production' ? false : debug
|
|
|
|
// Always provide a default table state
|
|
const defaultState = useTableState()
|
|
|
|
// But use the users table state if provided
|
|
const state = userState || defaultState
|
|
|
|
// The table instance ref
|
|
let instanceRef = React.useRef({})
|
|
|
|
Object.assign(instanceRef.current, {
|
|
...props,
|
|
data, // The raw data
|
|
state, // The resolved table state
|
|
plugins, // All resolved plugins
|
|
hooks: {
|
|
columnsBeforeHeaderGroups: [],
|
|
columnsBeforeHeaderGroupsDeps: [],
|
|
useMain: [],
|
|
useRows: [],
|
|
prepareRow: [],
|
|
getTableProps: [],
|
|
getTableBodyProps: [],
|
|
getRowProps: [],
|
|
getHeaderGroupProps: [],
|
|
getHeaderProps: [],
|
|
getCellProps: [],
|
|
},
|
|
})
|
|
|
|
// Allow plugins to register hooks
|
|
if (process.env.NODE_ENV === 'development' && debug) console.time('plugins')
|
|
|
|
plugins.filter(Boolean).forEach(plugin => {
|
|
plugin(instanceRef.current.hooks)
|
|
})
|
|
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.timeEnd('plugins')
|
|
|
|
// Decorate All the columns
|
|
let columns = React.useMemo(
|
|
() => decorateColumnTree(userColumns, defaultColumn),
|
|
[defaultColumn, userColumns]
|
|
)
|
|
|
|
// Get the flat list of all columns andllow hooks to decorate
|
|
// those columns (and trigger this memoization via deps)
|
|
let flatColumns = React.useMemo(() => {
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.time('hooks.columnsBeforeHeaderGroups')
|
|
|
|
let newColumns = applyHooks(
|
|
instanceRef.current.hooks.columnsBeforeHeaderGroups,
|
|
flattenBy(columns, 'columns'),
|
|
instanceRef.current
|
|
)
|
|
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.timeEnd('hooks.columnsBeforeHeaderGroups')
|
|
return newColumns
|
|
}, [
|
|
columns,
|
|
debug,
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
...applyHooks(
|
|
instanceRef.current.hooks.columnsBeforeHeaderGroupsDeps,
|
|
[],
|
|
instanceRef.current
|
|
),
|
|
])
|
|
|
|
// Make the headerGroups
|
|
const headerGroups = React.useMemo(
|
|
() => makeHeaderGroups(flatColumns, defaultColumn),
|
|
[defaultColumn, flatColumns]
|
|
)
|
|
|
|
const headers = React.useMemo(() => headerGroups[0].headers, [headerGroups])
|
|
|
|
Object.assign(instanceRef.current, {
|
|
columns,
|
|
flatColumns,
|
|
headerGroups,
|
|
headers,
|
|
})
|
|
|
|
// Access the row model
|
|
const [rows, rowPaths, flatRows] = React.useMemo(() => {
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.time('getAccessedRows')
|
|
|
|
let flatRows = 0
|
|
const rowPaths = []
|
|
|
|
// Access the row's data
|
|
const accessRow = (originalRow, i, depth = 0, parentPath = []) => {
|
|
// Keep the original reference around
|
|
const original = originalRow
|
|
|
|
const rowID = getRowID(originalRow, i)
|
|
|
|
// Make the new path for the row
|
|
const path = [...parentPath, rowID]
|
|
|
|
flatRows++
|
|
rowPaths.push(path.join('.'))
|
|
|
|
// Process any subRows
|
|
let subRows = getSubRows(originalRow, i)
|
|
|
|
if (subRows) {
|
|
subRows = subRows.map((d, i) => accessRow(d, i, depth + 1, path))
|
|
}
|
|
|
|
const row = {
|
|
original,
|
|
index: i,
|
|
path, // 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 = {}
|
|
flatColumns.forEach(column => {
|
|
row.values[column.id] = column.accessor
|
|
? column.accessor(originalRow, i, { subRows, depth, data })
|
|
: undefined
|
|
})
|
|
|
|
return row
|
|
}
|
|
|
|
// Use the resolved data
|
|
const accessedData = data.map((d, i) => accessRow(d, i))
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.timeEnd('getAccessedRows')
|
|
return [accessedData, rowPaths, flatRows]
|
|
}, [debug, data, getRowID, getSubRows, flatColumns])
|
|
|
|
instanceRef.current.rows = rows
|
|
instanceRef.current.rowPaths = rowPaths
|
|
instanceRef.current.flatRows = flatRows
|
|
|
|
// Determine column visibility
|
|
determineHeaderVisibility(instanceRef.current)
|
|
|
|
// Provide a flat header list for utilities
|
|
instanceRef.current.flatHeaders = headerGroups.reduce(
|
|
(all, headerGroup) => [...all, ...headerGroup.headers],
|
|
[]
|
|
)
|
|
|
|
calculateDimensions(instanceRef.current)
|
|
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.time('hooks.useMain')
|
|
instanceRef.current = applyHooks(
|
|
instanceRef.current.hooks.useMain,
|
|
instanceRef.current
|
|
)
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.timeEnd('hooks.useMain')
|
|
|
|
// Each materialized header needs to be assigned a render function and other
|
|
// prop getter properties here.
|
|
instanceRef.current.flatHeaders.forEach(column => {
|
|
// Give columns/headers rendering power
|
|
column.render = (type, userProps = {}) => {
|
|
const Comp = typeof type === 'string' ? column[type] : type
|
|
|
|
if (typeof Comp === 'undefined') {
|
|
throw new Error(renderErr)
|
|
}
|
|
|
|
return flexRender(Comp, {
|
|
...instanceRef.current,
|
|
column,
|
|
...userProps,
|
|
})
|
|
}
|
|
|
|
// Give columns/headers a default getHeaderProps
|
|
column.getHeaderProps = props =>
|
|
mergeProps(
|
|
{
|
|
key: ['header', column.id].join('_'),
|
|
colSpan: column.totalVisibleHeaderCount,
|
|
},
|
|
applyPropHooks(
|
|
instanceRef.current.hooks.getHeaderProps,
|
|
column,
|
|
instanceRef.current
|
|
),
|
|
props
|
|
)
|
|
})
|
|
|
|
instanceRef.current.headerGroups.forEach((headerGroup, i) => {
|
|
// Filter out any headers and headerGroups that don't have visible columns
|
|
headerGroup.headers = headerGroup.headers.filter(header => {
|
|
const recurse = headers =>
|
|
headers.filter(header => {
|
|
if (header.headers) {
|
|
return recurse(header.headers)
|
|
}
|
|
return header.isVisible
|
|
}).length
|
|
if (header.headers) {
|
|
return recurse(header.headers)
|
|
}
|
|
return header.isVisible
|
|
})
|
|
|
|
// Give headerGroups getRowProps
|
|
if (headerGroup.headers.length) {
|
|
headerGroup.getHeaderGroupProps = (props = {}) =>
|
|
mergeProps(
|
|
{
|
|
key: [`header${i}`].join('_'),
|
|
},
|
|
applyPropHooks(
|
|
instanceRef.current.hooks.getHeaderGroupProps,
|
|
headerGroup,
|
|
instanceRef.current
|
|
),
|
|
props
|
|
)
|
|
|
|
return true
|
|
}
|
|
})
|
|
|
|
// Run the rows (this could be a dangerous hook with a ton of data)
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.time('hooks.useRows')
|
|
instanceRef.current.rows = applyHooks(
|
|
instanceRef.current.hooks.useRows,
|
|
instanceRef.current.rows,
|
|
instanceRef.current
|
|
)
|
|
if (process.env.NODE_ENV === 'development' && debug)
|
|
console.timeEnd('hooks.useRows')
|
|
|
|
// The prepareRow function is absolutely necessary and MUST be called on
|
|
// any rows the user wishes to be displayed.
|
|
|
|
instanceRef.current.prepareRow = React.useCallback(row => {
|
|
row.getRowProps = props =>
|
|
mergeProps(
|
|
{ key: ['row', ...row.path].join('_') },
|
|
applyPropHooks(
|
|
instanceRef.current.hooks.getRowProps,
|
|
row,
|
|
instanceRef.current
|
|
),
|
|
props
|
|
)
|
|
|
|
// Build the visible cells for each row
|
|
row.cells = instanceRef.current.flatColumns
|
|
.filter(d => d.isVisible)
|
|
.map(column => {
|
|
const cell = {
|
|
column,
|
|
row,
|
|
value: row.values[column.id],
|
|
}
|
|
|
|
// Give each cell a getCellProps base
|
|
cell.getCellProps = props => {
|
|
const columnPathStr = [...row.path, column.id].join('_')
|
|
return mergeProps(
|
|
{
|
|
key: ['cell', columnPathStr].join('_'),
|
|
},
|
|
applyPropHooks(
|
|
instanceRef.current.hooks.getCellProps,
|
|
cell,
|
|
instanceRef.current
|
|
),
|
|
props
|
|
)
|
|
}
|
|
|
|
// Give each cell a renderer function (supports multiple renderers)
|
|
cell.render = (type, userProps = {}) => {
|
|
const Comp = typeof type === 'string' ? column[type] : type
|
|
|
|
if (typeof Comp === 'undefined') {
|
|
throw new Error(renderErr)
|
|
}
|
|
|
|
return flexRender(Comp, {
|
|
...instanceRef.current,
|
|
column,
|
|
row,
|
|
cell,
|
|
...userProps,
|
|
})
|
|
}
|
|
|
|
return cell
|
|
})
|
|
|
|
// need to apply any row specific hooks (useExpanded requires this)
|
|
applyHooks(instanceRef.current.hooks.prepareRow, row, instanceRef.current)
|
|
}, [])
|
|
|
|
instanceRef.current.getTableProps = userProps =>
|
|
mergeProps(
|
|
applyPropHooks(
|
|
instanceRef.current.hooks.getTableProps,
|
|
instanceRef.current
|
|
),
|
|
userProps
|
|
)
|
|
|
|
instanceRef.current.getTableBodyProps = userProps =>
|
|
mergeProps(
|
|
applyPropHooks(
|
|
instanceRef.current.hooks.getTableBodyProps,
|
|
instanceRef.current
|
|
),
|
|
userProps
|
|
)
|
|
|
|
return instanceRef.current
|
|
}
|
|
|
|
function calculateDimensions(instance) {
|
|
const { headers } = instance
|
|
|
|
instance.totalColumnsWidth = calculateHeaderWidths(headers)
|
|
}
|
|
|
|
function calculateHeaderWidths(headers, left = 0) {
|
|
let sumTotalWidth = 0
|
|
|
|
headers.forEach(header => {
|
|
let { headers: subHeaders } = header
|
|
|
|
header.totalLeft = left
|
|
|
|
if (subHeaders && subHeaders.length) {
|
|
header.totalWidth = calculateHeaderWidths(subHeaders, left)
|
|
} else {
|
|
header.totalWidth = Math.min(
|
|
Math.max(header.minWidth, header.width),
|
|
header.maxWidth
|
|
)
|
|
}
|
|
|
|
left += header.totalWidth
|
|
sumTotalWidth += header.totalWidth
|
|
})
|
|
|
|
return sumTotalWidth
|
|
}
|