UNPKG

react-table-v6

Version:

A fast, lightweight, opinionated table and datagrid built on React

865 lines (793 loc) 28.4 kB
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() } }