react-table-v6
Version:
A fast, lightweight, opinionated table and datagrid built on React
865 lines (793 loc) • 28.4 kB
JavaScript
import React, { Component } from 'react'
import classnames from 'classnames'
//
import _ from './utils'
import Lifecycle from './lifecycle'
import Methods from './methods'
import defaultProps from './defaultProps'
import propTypes from './propTypes'
export const ReactTableDefaults = defaultProps
export default class ReactTable extends Methods(Lifecycle(Component)) {
static propTypes = propTypes
static defaultProps = defaultProps
constructor (props) {
super()
this.getResolvedState = this.getResolvedState.bind(this)
this.getDataModel = this.getDataModel.bind(this)
this.getSortedData = this.getSortedData.bind(this)
this.fireFetchData = this.fireFetchData.bind(this)
this.getPropOrState = this.getPropOrState.bind(this)
this.getStateOrProp = this.getStateOrProp.bind(this)
this.filterData = this.filterData.bind(this)
this.sortData = this.sortData.bind(this)
this.getMinRows = this.getMinRows.bind(this)
this.onPageChange = this.onPageChange.bind(this)
this.onPageSizeChange = this.onPageSizeChange.bind(this)
this.sortColumn = this.sortColumn.bind(this)
this.filterColumn = this.filterColumn.bind(this)
this.resizeColumnStart = this.resizeColumnStart.bind(this)
this.resizeColumnEnd = this.resizeColumnEnd.bind(this)
this.resizeColumnMoving = this.resizeColumnMoving.bind(this)
this.state = {
page: 0,
pageSize: props.defaultPageSize,
sorted: props.defaultSorted,
expanded: props.defaultExpanded,
filtered: props.defaultFiltered,
resized: props.defaultResized,
currentlyResizing: false,
skipNextSort: false,
}
}
render () {
const resolvedState = this.getResolvedState()
const {
children,
className,
style,
getProps,
getTableProps,
getTheadGroupProps,
getTheadGroupTrProps,
getTheadGroupThProps,
getTheadProps,
getTheadTrProps,
getTheadThProps,
getTheadFilterProps,
getTheadFilterTrProps,
getTheadFilterThProps,
getTbodyProps,
getTrGroupProps,
getTrProps,
getTdProps,
getTfootProps,
getTfootTrProps,
getTfootTdProps,
getPaginationProps,
getLoadingProps,
getNoDataProps,
getResizerProps,
showPagination,
showPaginationTop,
showPaginationBottom,
manual,
loadingText,
noDataText,
sortable,
multiSort,
resizable,
filterable,
// Pivoting State
pivotIDKey,
pivotValKey,
pivotBy,
subRowsKey,
aggregatedKey,
originalKey,
indexKey,
groupedByPivotKey,
// State
loading,
pageSize,
page,
sorted,
filtered,
resized,
expanded,
pages,
onExpandedChange,
// Components
TableComponent,
TheadComponent,
TbodyComponent,
TrGroupComponent,
TrComponent,
ThComponent,
TdComponent,
TfootComponent,
PaginationComponent,
LoadingComponent,
SubComponent,
NoDataComponent,
ResizerComponent,
ExpanderComponent,
PivotValueComponent,
PivotComponent,
AggregatedComponent,
FilterComponent,
PadRowComponent,
// Data model
resolvedData,
allVisibleColumns,
headerGroups,
hasHeaderGroups,
// Sorted Data
sortedData,
currentlyResizing,
} = resolvedState
// Pagination
const startRow = pageSize * page
const endRow = startRow + pageSize
let pageRows = manual ? resolvedData : sortedData.slice(startRow, endRow)
const minRows = this.getMinRows()
const padRows = _.range(Math.max(minRows - pageRows.length, 0))
const hasColumnFooter = allVisibleColumns.some(d => d.Footer)
const hasFilters = filterable || allVisibleColumns.some(d => d.filterable)
const recurseRowsViewIndex = (rows, path = [], index = -1) => [
rows.map((row, i) => {
index += 1
const rowWithViewIndex = {
...row,
_viewIndex: index,
}
const newPath = path.concat([i])
if (rowWithViewIndex[subRowsKey] && _.get(expanded, newPath)) {
[rowWithViewIndex[subRowsKey], index] = recurseRowsViewIndex(
rowWithViewIndex[subRowsKey],
newPath,
index
)
}
return rowWithViewIndex
}),
index,
];
[pageRows] = recurseRowsViewIndex(pageRows)
const canPrevious = page > 0
const canNext = page + 1 < pages
const rowMinWidth = _.sum(
allVisibleColumns.map(d => {
const resizedColumn = resized.find(x => x.id === d.id) || {}
return _.getFirstDefined(resizedColumn.value, d.width, d.minWidth)
})
)
let rowIndex = -1
const finalState = {
...resolvedState,
startRow,
endRow,
pageRows,
minRows,
padRows,
hasColumnFooter,
canPrevious,
canNext,
rowMinWidth,
}
const rootProps = _.splitProps(getProps(finalState, undefined, undefined, this))
const tableProps = _.splitProps(getTableProps(finalState, undefined, undefined, this))
const tBodyProps = _.splitProps(getTbodyProps(finalState, undefined, undefined, this))
const loadingProps = getLoadingProps(finalState, undefined, undefined, this)
const noDataProps = getNoDataProps(finalState, undefined, undefined, this)
// Visual Components
const makeHeaderGroup = (column, i) => {
const resizedValue = col => (resized.find(x => x.id === col.id) || {}).value
const flex = _.sum(
column.columns.map(col => (col.width || resizedValue(col) ? 0 : col.minWidth))
)
const width = _.sum(
column.columns.map(col => _.getFirstDefined(resizedValue(col), col.width, col.minWidth))
)
const maxWidth = _.sum(
column.columns.map(col => _.getFirstDefined(resizedValue(col), col.width, col.maxWidth))
)
const theadGroupThProps = _.splitProps(
getTheadGroupThProps(finalState, undefined, column, this)
)
const columnHeaderProps = _.splitProps(
column.getHeaderProps(finalState, undefined, column, this)
)
const classes = [
column.headerClassName,
theadGroupThProps.className,
columnHeaderProps.className,
]
const styles = {
...column.headerStyle,
...theadGroupThProps.style,
...columnHeaderProps.style,
}
const rest = {
...theadGroupThProps.rest,
...columnHeaderProps.rest,
}
const flexStyles = {
flex: `${flex} 0 auto`,
width: _.asPx(width),
maxWidth: _.asPx(maxWidth),
}
return (
<ThComponent
key={`${i}-${column.id}`}
className={classnames(classes)}
style={{
...styles,
...flexStyles,
}}
{...rest}
>
{_.normalizeComponent(column.Header, {
data: sortedData,
column,
})}
</ThComponent>
)
}
const makeHeaderGroups = () => {
const theadGroupProps = _.splitProps(
getTheadGroupProps(finalState, undefined, undefined, this)
)
const theadGroupTrProps = _.splitProps(
getTheadGroupTrProps(finalState, undefined, undefined, this)
)
return (
<TheadComponent
className={classnames('-headerGroups', theadGroupProps.className)}
style={{
...theadGroupProps.style,
minWidth: `${rowMinWidth}px`,
}}
{...theadGroupProps.rest}
>
<TrComponent
className={theadGroupTrProps.className}
style={theadGroupTrProps.style}
{...theadGroupTrProps.rest}
>
{headerGroups.map(makeHeaderGroup)}
</TrComponent>
</TheadComponent>
)
}
const makeHeader = (column, i) => {
const resizedCol = resized.find(x => x.id === column.id) || {}
const sort = sorted.find(d => d.id === column.id)
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resizedCol.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resizedCol.value, column.width, column.maxWidth)
const theadThProps = _.splitProps(getTheadThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(
column.getHeaderProps(finalState, undefined, column, this)
)
const classes = [column.headerClassName, theadThProps.className, columnHeaderProps.className]
const styles = {
...column.headerStyle,
...theadThProps.style,
...columnHeaderProps.style,
}
const rest = {
...theadThProps.rest,
...columnHeaderProps.rest,
}
const isResizable = _.getFirstDefined(column.resizable, resizable, false)
const resizer = isResizable ? (
<ResizerComponent
onMouseDown={e => this.resizeColumnStart(e, column, false)}
onTouchStart={e => this.resizeColumnStart(e, column, true)}
{...getResizerProps('finalState', undefined, column, this)}
/>
) : null
const isSortable = _.getFirstDefined(column.sortable, sortable, false)
return (
<ThComponent
key={`${i}-${column.id}`}
className={classnames(
classes,
isResizable && 'rt-resizable-header',
sort ? (sort.desc ? '-sort-desc' : '-sort-asc') : '',
isSortable && '-cursor-pointer',
!show && '-hidden',
pivotBy && pivotBy.slice(0, -1).includes(column.id) && 'rt-header-pivot'
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: _.asPx(width),
maxWidth: _.asPx(maxWidth),
}}
toggleSort={e => {
if (isSortable) this.sortColumn(column, multiSort ? e.shiftKey : false)
}}
{...rest}
>
<div className={classnames(isResizable && 'rt-resizable-header-content')}>
{_.normalizeComponent(column.Header, {
data: sortedData,
column,
})}
</div>
{resizer}
</ThComponent>
)
}
const makeHeaders = () => {
const theadProps = _.splitProps(getTheadProps(finalState, undefined, undefined, this))
const theadTrProps = _.splitProps(getTheadTrProps(finalState, undefined, undefined, this))
return (
<TheadComponent
className={classnames('-header', theadProps.className)}
style={{
...theadProps.style,
minWidth: `${rowMinWidth}px`,
}}
{...theadProps.rest}
>
<TrComponent
className={theadTrProps.className}
style={theadTrProps.style}
{...theadTrProps.rest}
>
{allVisibleColumns.map(makeHeader)}
</TrComponent>
</TheadComponent>
)
}
const makeFilter = (column, i) => {
const resizedCol = resized.find(x => x.id === column.id) || {}
const width = _.getFirstDefined(resizedCol.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resizedCol.value, column.width, column.maxWidth)
const theadFilterThProps = _.splitProps(
getTheadFilterThProps(finalState, undefined, column, this)
)
const columnHeaderProps = _.splitProps(
column.getHeaderProps(finalState, undefined, column, this)
)
const classes = [
column.headerClassName,
theadFilterThProps.className,
columnHeaderProps.className,
]
const styles = {
...column.headerStyle,
...theadFilterThProps.style,
...columnHeaderProps.style,
}
const rest = {
...theadFilterThProps.rest,
...columnHeaderProps.rest,
}
const filter = filtered.find(filter => filter.id === column.id)
const ResolvedFilterComponent = column.Filter || FilterComponent
const isFilterable = _.getFirstDefined(column.filterable, filterable, false)
return (
<ThComponent
key={`${i}-${column.id}`}
className={classnames(classes)}
style={{
...styles,
flex: `${width} 0 auto`,
width: _.asPx(width),
maxWidth: _.asPx(maxWidth),
}}
{...rest}
>
{isFilterable
? _.normalizeComponent(
ResolvedFilterComponent,
{
column,
filter,
onChange: value => this.filterColumn(column, value),
},
defaultProps.column.Filter
)
: null}
</ThComponent>
)
}
const makeFilters = () => {
const theadFilterProps = _.splitProps(
getTheadFilterProps(finalState, undefined, undefined, this)
)
const theadFilterTrProps = _.splitProps(
getTheadFilterTrProps(finalState, undefined, undefined, this)
)
return (
<TheadComponent
className={classnames('-filters', theadFilterProps.className)}
style={{
...theadFilterProps.style,
minWidth: `${rowMinWidth}px`,
}}
{...theadFilterProps.rest}
>
<TrComponent
className={theadFilterTrProps.className}
style={theadFilterTrProps.style}
{...theadFilterTrProps.rest}
>
{allVisibleColumns.map(makeFilter)}
</TrComponent>
</TheadComponent>
)
}
const makePageRow = (row, i, path = []) => {
const rowInfo = {
original: row[originalKey],
row,
index: row[indexKey],
viewIndex: (rowIndex += 1),
pageSize,
page,
level: path.length,
nestingPath: path.concat([i]),
aggregated: row[aggregatedKey],
groupedByPivot: row[groupedByPivotKey],
subRows: row[subRowsKey],
}
const isExpanded = _.get(expanded, rowInfo.nestingPath)
const trGroupProps = getTrGroupProps(finalState, rowInfo, undefined, this)
const trProps = _.splitProps(getTrProps(finalState, rowInfo, undefined, this))
return (
<TrGroupComponent key={rowInfo.nestingPath.join('_')} {...trGroupProps}>
<TrComponent
className={classnames(trProps.className, row._viewIndex % 2 ? '-even' : '-odd')}
style={trProps.style}
{...trProps.rest}
>
{allVisibleColumns.map((column, i2) => {
const resizedCol = resized.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resizedCol.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resizedCol.value, column.width, column.maxWidth)
const tdProps = _.splitProps(getTdProps(finalState, rowInfo, column, this))
const columnProps = _.splitProps(column.getProps(finalState, rowInfo, column, this))
const classes = [tdProps.className, column.className, columnProps.className]
const styles = {
...tdProps.style,
...column.style,
...columnProps.style,
}
const cellInfo = {
...rowInfo,
isExpanded,
column: { ...column },
value: rowInfo.row[column.id],
pivoted: column.pivoted,
expander: column.expander,
resized,
show,
width,
maxWidth,
tdProps,
columnProps,
classes,
styles,
}
const value = cellInfo.value
let useOnExpanderClick
let isBranch
let isPreview
const onExpanderClick = e => {
let newExpanded = _.clone(expanded)
if (isExpanded) {
newExpanded = _.set(newExpanded, cellInfo.nestingPath, false)
} else {
newExpanded = _.set(newExpanded, cellInfo.nestingPath, {})
}
return this.setStateWithData(
{
expanded: newExpanded,
},
() => onExpandedChange && onExpandedChange(newExpanded, cellInfo.nestingPath, e)
)
}
// Default to a standard cell
let resolvedCell = _.normalizeComponent(column.Cell, cellInfo, value)
// Resolve Renderers
const ResolvedAggregatedComponent =
column.Aggregated || (!column.aggregate ? AggregatedComponent : column.Cell)
const ResolvedExpanderComponent = column.Expander || ExpanderComponent
const ResolvedPivotValueComponent = column.PivotValue || PivotValueComponent
const DefaultResolvedPivotComponent =
PivotComponent ||
(props => (
<div>
<ResolvedExpanderComponent {...props} />
<ResolvedPivotValueComponent {...props} />
</div>
))
const ResolvedPivotComponent = column.Pivot || DefaultResolvedPivotComponent
// Is this cell expandable?
if (cellInfo.pivoted || cellInfo.expander) {
// Make it expandable by defualt
cellInfo.expandable = true
useOnExpanderClick = true
// If pivoted, has no subRows, and does not have a subComponent,
// do not make expandable
if (cellInfo.pivoted && !cellInfo.subRows && !SubComponent) {
cellInfo.expandable = false
}
}
if (cellInfo.pivoted) {
// Is this column a branch?
isBranch = rowInfo.row[pivotIDKey] === column.id && cellInfo.subRows
// Should this column be blank?
isPreview =
pivotBy.indexOf(column.id) > pivotBy.indexOf(rowInfo.row[pivotIDKey]) &&
cellInfo.subRows
// Pivot Cell Render Override
if (isBranch) {
// isPivot
resolvedCell = _.normalizeComponent(
ResolvedPivotComponent,
{
...cellInfo,
value: row[pivotValKey],
},
row[pivotValKey]
)
} else if (isPreview) {
// Show the pivot preview
resolvedCell = _.normalizeComponent(ResolvedAggregatedComponent, cellInfo, value)
} else {
resolvedCell = null
}
} else if (cellInfo.aggregated) {
resolvedCell = _.normalizeComponent(ResolvedAggregatedComponent, cellInfo, value)
}
if (cellInfo.expander) {
resolvedCell = _.normalizeComponent(
ResolvedExpanderComponent,
cellInfo,
row[pivotValKey]
)
if (pivotBy) {
if (cellInfo.groupedByPivot) {
resolvedCell = null
}
if (!cellInfo.subRows && !SubComponent) {
resolvedCell = null
}
}
}
const resolvedOnExpanderClick = useOnExpanderClick ? onExpanderClick : () => {}
// If there are multiple onClick events, make sure they don't
// override eachother. This should maybe be expanded to handle all
// function attributes
const interactionProps = {
onClick: resolvedOnExpanderClick,
}
if (tdProps.rest.onClick) {
interactionProps.onClick = e => {
tdProps.rest.onClick(e, () => resolvedOnExpanderClick(e))
}
}
if (columnProps.rest.onClick) {
interactionProps.onClick = e => {
columnProps.rest.onClick(e, () => resolvedOnExpanderClick(e))
}
}
// Return the cell
return (
<TdComponent
// eslint-disable-next-line react/no-array-index-key
key={`${i2}-${column.id}`}
className={classnames(
classes,
!show && 'hidden',
cellInfo.expandable && 'rt-expandable',
(isBranch || isPreview) && 'rt-pivot'
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: _.asPx(width),
maxWidth: _.asPx(maxWidth),
}}
{...tdProps.rest}
{...columnProps.rest}
{...interactionProps}
>
{resolvedCell}
</TdComponent>
)
})}
</TrComponent>
{rowInfo.subRows &&
isExpanded &&
rowInfo.subRows.map((d, i) => makePageRow(d, i, rowInfo.nestingPath))}
{SubComponent && !rowInfo.subRows && isExpanded && SubComponent(rowInfo)}
</TrGroupComponent>
)
}
const makePadColumn = (column, i) => {
const resizedCol = resized.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resizedCol.value, column.width, column.minWidth)
const flex = width
const maxWidth = _.getFirstDefined(resizedCol.value, column.width, column.maxWidth)
const tdProps = _.splitProps(getTdProps(finalState, undefined, column, this))
const columnProps = _.splitProps(column.getProps(finalState, undefined, column, this))
const classes = [tdProps.className, column.className, columnProps.className]
const styles = {
...tdProps.style,
...column.style,
...columnProps.style,
}
return (
<TdComponent
key={`${i}-${column.id}`}
className={classnames(classes, !show && 'hidden')}
style={{
...styles,
flex: `${flex} 0 auto`,
width: _.asPx(width),
maxWidth: _.asPx(maxWidth),
}}
{...tdProps.rest}
>
{_.normalizeComponent(PadRowComponent)}
</TdComponent>
)
}
const makePadRow = (row, i) => {
const trGroupProps = getTrGroupProps(finalState, undefined, undefined, this)
const trProps = _.splitProps(getTrProps(finalState, undefined, undefined, this))
return (
<TrGroupComponent key={i} {...trGroupProps}>
<TrComponent
className={classnames(
'-padRow',
(pageRows.length + i) % 2 ? '-even' : '-odd',
trProps.className
)}
style={trProps.style || {}}
>
{allVisibleColumns.map(makePadColumn)}
</TrComponent>
</TrGroupComponent>
)
}
const makeColumnFooter = (column, i) => {
const resizedCol = resized.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resizedCol.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resizedCol.value, column.width, column.maxWidth)
const tFootTdProps = _.splitProps(getTfootTdProps(finalState, undefined, undefined, this))
const columnProps = _.splitProps(column.getProps(finalState, undefined, column, this))
const columnFooterProps = _.splitProps(
column.getFooterProps(finalState, undefined, column, this)
)
const classes = [
tFootTdProps.className,
column.className,
columnProps.className,
columnFooterProps.className,
]
const styles = {
...tFootTdProps.style,
...column.style,
...columnProps.style,
...columnFooterProps.style,
}
return (
<TdComponent
key={`${i}-${column.id}`}
className={classnames(classes, !show && 'hidden')}
style={{
...styles,
flex: `${width} 0 auto`,
width: _.asPx(width),
maxWidth: _.asPx(maxWidth),
}}
{...columnProps.rest}
{...tFootTdProps.rest}
{...columnFooterProps.rest}
>
{_.normalizeComponent(column.Footer, {
data: sortedData,
column,
})}
</TdComponent>
)
}
const makeColumnFooters = () => {
const tFootProps = getTfootProps(finalState, undefined, undefined, this)
const tFootTrProps = _.splitProps(getTfootTrProps(finalState, undefined, undefined, this))
return (
<TfootComponent
className={tFootProps.className}
style={{
...tFootProps.style,
minWidth: `${rowMinWidth}px`,
}}
{...tFootProps.rest}
>
<TrComponent
className={classnames(tFootTrProps.className)}
style={tFootTrProps.style}
{...tFootTrProps.rest}
>
{allVisibleColumns.map(makeColumnFooter)}
</TrComponent>
</TfootComponent>
)
}
const makePagination = () => {
const paginationProps = _.splitProps(
getPaginationProps(finalState, undefined, undefined, this)
)
return (
<PaginationComponent
{...resolvedState}
pages={pages}
canPrevious={canPrevious}
canNext={canNext}
onPageChange={this.onPageChange}
onPageSizeChange={this.onPageSizeChange}
className={paginationProps.className}
style={paginationProps.style}
{...paginationProps.rest}
/>
)
}
const makeTable = () => {
const pagination = makePagination()
return (
<div
className={classnames('ReactTable', className, rootProps.className)}
style={{
...style,
...rootProps.style,
}}
{...rootProps.rest}
>
{showPagination && showPaginationTop ? (
<div className="pagination-top">{pagination}</div>
) : null}
<TableComponent
className={classnames(tableProps.className, currentlyResizing ? 'rt-resizing' : '')}
style={tableProps.style}
{...tableProps.rest}
>
{hasHeaderGroups ? makeHeaderGroups() : null}
{makeHeaders()}
{hasFilters ? makeFilters() : null}
<TbodyComponent
className={classnames(tBodyProps.className)}
style={{
...tBodyProps.style,
minWidth: `${rowMinWidth}px`,
}}
{...tBodyProps.rest}
>
{pageRows.map((d, i) => makePageRow(d, i))}
{padRows.map(makePadRow)}
</TbodyComponent>
{hasColumnFooter ? makeColumnFooters() : null}
</TableComponent>
{showPagination && showPaginationBottom ? (
<div className="pagination-bottom">{pagination}</div>
) : null}
{!pageRows.length && (
<NoDataComponent {...noDataProps}>{_.normalizeComponent(noDataText)}</NoDataComponent>
)}
<LoadingComponent loading={loading} loadingText={loadingText} {...loadingProps} />
</div>
)
}
// childProps are optionally passed to a function-as-a-child
return children ? children(finalState, makeTable, this) : makeTable()
}
}