diff --git a/CHANGELOG.md b/CHANGELOG.md index cd37e14..92c87d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.3.0 +##### New Features +- `defaultSortDesc` - allows you to set the default sorting direction for all columns to descending. +- `column.defaultSortDesc` - allows you to set the default sorting direction for a specific column. Falls back to the global `defaultSortDesc` when not set at all. + ## 6.0.0 ##### New Features diff --git a/README.md b/README.md index b5b0e3f..056a103 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ These are all of the available props (and their default values) for the main ` ({}), filterMethod: undefined, - sortMethod: undefined + sortMethod: undefined, + defaultSortDesc: undefined, }, // Global Expander Column Defaults @@ -804,7 +806,11 @@ Accessing internal state and wrapping with more UI: The possibilities are endless! ## Sorting -Sorting comes built in with React-Table. Click column header to sort by its column. Click it again to reverse the sort. +Sorting comes built in with React-Table. +- Click a column header to sort by its accessor. +- Click it again to reverse the sort. +- Set `defaultSortDesc` property to `true` to make the first sort direction default to descending. +- Override a specific column's default sort direction by using the same `defaultSortDesc` property on a column, set to `true` ## Multi-Sort When clicking on a column header, hold shift to multi-sort! You can toggle `ascending` `descending` and `none` for multi-sort columns. Clicking on a header without holding shift will clear the multi-sort and replace it with the single sort of that column. It's quite handy! diff --git a/docs/src/stories/Simple.js b/docs/src/stories/Simple.js index 293a1a3..11e3bb3 100644 --- a/docs/src/stories/Simple.js +++ b/docs/src/stories/Simple.js @@ -6,7 +6,7 @@ import namor from 'namor' import ReactTable from '../../../lib/index' class Story extends React.PureComponent { - render () { + render() { const data = _.map(_.range(5553), d => { return { firstName: namor.generate({ words: 1, numbers: 0 }), @@ -22,35 +22,43 @@ class Story extends React.PureComponent { } }) - const columns = [{ - Header: 'Name', - columns: [{ - Header: 'First Name', - accessor: 'firstName' - }, { - Header: 'Last Name', - id: 'lastName', - accessor: d => d.lastName - }] - }, { - Header: 'Info', - columns: [{ - Header: 'Age', - accessor: 'age' - }] - }] + const columns = [ + { + Header: 'Name', + columns: [ + { + Header: 'First Name', + accessor: 'firstName' + }, + { + Header: 'Last Name', + id: 'lastName', + accessor: d => d.lastName + } + ] + }, + { + Header: 'Info', + columns: [ + { + Header: 'Age', + accessor: 'age' + } + ] + } + ] return (
-
+
-
+

Tip: Hold shift when sorting to multi-sort!
@@ -62,9 +70,8 @@ class Story extends React.PureComponent { const CodeHighlight = require('./components/codeHighlight').default const source = require('!raw!./Simple') -export default () => ( +export default () =>
{() => source}
-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 8116c76..aa27a9b 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -4557,14 +4557,16 @@ react-json-tree@^0.10.9: prop-types "^15.5.8" react-base16-styling "^0.5.1" -react-router-dom@next: - version "4.0.0-beta.8" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.0.0-beta.8.tgz#907a5a0a36e9190652c80f2feead0eadbbba262e" +react-router-dom@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.1.1.tgz#3021ade1f2c160af97cf94e25594c5f294583025" dependencies: history "^4.5.1" - react-router "^4.0.0-beta.8" + loose-envify "^1.3.1" + prop-types "^15.5.4" + react-router "^4.1.1" -react-router@^4.0.0-beta.8: +react-router@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.1.1.tgz#d448f3b7c1b429a6fbb03395099949c606b1fe95" dependencies: @@ -4621,9 +4623,9 @@ react-scripts@0.9.5: optionalDependencies: fsevents "1.0.17" -react-story@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/react-story/-/react-story-0.0.6.tgz#ec3e6b42e5edab8a74dd581c12bd321ec304eaba" +react-story@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/react-story/-/react-story-0.0.10.tgz#4c07d122962900162f0871666780da34402a2573" dependencies: classnames "^2.2.5" glamor "^2.20.25" @@ -4631,7 +4633,7 @@ react-story@^0.0.6: javascript-detect-element-resize "^0.5.3" raf "^3.3.2" react-fastclick "^3.0.1" - react-router-dom next + react-router-dom "^4.1.1" react@^15.5.4: version "15.5.4" diff --git a/package.json b/package.json index d0fd444..a69324a 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "build": "npm-run-all build:*", "prepublish": "npm run build && npm run umd", "postpublish": "git push --tags", - "docs": "cd docs && yarn && yarn start", - "docs:build": "cd docs && yarn && yarn run build" + "docs": "yarn watch & cd docs && yarn && yarn start", + "docs:build": "yarn build && cd docs && yarn && yarn run build" }, "dependencies": { "classnames": "^2.2.5", diff --git a/src/defaultProps.js b/src/defaultProps.js index 2a29bc9..d5f2e74 100644 --- a/src/defaultProps.js +++ b/src/defaultProps.js @@ -24,18 +24,21 @@ export default { sortable: true, resizable: true, filterable: false, + defaultSortDesc: false, defaultSorted: [], defaultFiltered: [], defaultResized: [], defaultExpanded: {}, defaultFilterMethod: (filter, row, column) => { const id = filter.pivotId || filter.id - return row[id] !== undefined ? String(row[id]).startsWith(filter.value) : true + return row[id] !== undefined + ? String(row[id]).startsWith(filter.value) + : true }, defaultSortMethod: (a, b) => { // force null and undefined to the bottom - a = (a === null || a === undefined) ? '' : a - b = (b === null || b === undefined) ? '' : b + a = a === null || a === undefined ? '' : a + b = b === null || b === undefined ? '' : b // force any string values to lowercase a = typeof a === 'string' ? a.toLowerCase() : a b = typeof b === 'string' ? b.toLowerCase() : b @@ -142,7 +145,7 @@ export default { footerStyle: {}, getFooterProps: emptyObj, filterMethod: undefined, - sortMethod: undefined + sortMethod: undefined, }, // Global Expander Column Defaults @@ -150,7 +153,7 @@ export default { sortable: false, resizable: false, filterable: false, - width: 35 + width: 35, }, pivotDefaults: { @@ -172,7 +175,7 @@ export default { TbodyComponent: _.makeTemplateComponent('rt-tbody'), TrGroupComponent: _.makeTemplateComponent('rt-tr-group'), TrComponent: _.makeTemplateComponent('rt-tr'), - ThComponent: ({toggleSort, className, children, ...rest}) => { + ThComponent: ({ toggleSort, className, children, ...rest }) => { return (
( - + onChange(event.target.value)} - /> - ), - ExpanderComponent: ({isExpanded}) => ( + onChange={event => onChange(event.target.value)} + />, + ExpanderComponent: ({ isExpanded }) =>
• -
- ), - PivotValueComponent: ({subRows, value}) => ( - {value} {subRows && `(${subRows.length})`} - ), - AggregatedComponent: ({subRows, column}) => { +
, + PivotValueComponent: ({ subRows, value }) => + {value} {subRows && `(${subRows.length})`}, + AggregatedComponent: ({ subRows, column }) => { const previewValues = subRows .filter(d => typeof d[column.id] !== 'undefined') - .map((row, i) => ( - {row[column.id]}{i < subRows.length - 1 ? ', ' : ''} - )) - return ( - {previewValues} - ) + .map((row, i) => + + {row[column.id]}{i < subRows.length - 1 ? ', ' : ''} + + ) + return {previewValues} }, PivotComponent: undefined, // this is a computed default generated using // the ExpanderComponent and PivotValueComponent at run-time in methods.js PaginationComponent: Pagination, PreviousComponent: undefined, NextComponent: undefined, - LoadingComponent: ({className, loading, loadingText, ...rest}) => ( -
+
{loadingText}
-
- ), +
, NoDataComponent: _.makeTemplateComponent('rt-noData'), - ResizerComponent: _.makeTemplateComponent('rt-resizer') + ResizerComponent: _.makeTemplateComponent('rt-resizer'), } diff --git a/src/index.js b/src/index.js index 496a762..9a9e3e6 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ export const ReactTableDefaults = defaultProps export default class ReactTable extends Methods(Lifecycle(Component)) { static defaultProps = defaultProps - constructor (props) { + constructor(props) { super() this.getResolvedState = this.getResolvedState.bind(this) @@ -43,7 +43,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { } } - render () { + render() { const resolvedState = this.getResolvedState() const { children, @@ -148,7 +148,11 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { } const newPath = path.concat([i]) if (rowWithViewIndex[subRowsKey] && _.get(expanded, newPath)) { - [rowWithViewIndex[subRowsKey], index] = recurseRowsViewIndex(rowWithViewIndex[subRowsKey], newPath, index) + ;[rowWithViewIndex[subRowsKey], index] = recurseRowsViewIndex( + rowWithViewIndex[subRowsKey], + newPath, + index + ) } return rowWithViewIndex }), @@ -156,15 +160,17 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ] } - [pageRows] = recurseRowsViewIndex(pageRows) + ;[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) - })) + 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 @@ -184,8 +190,12 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { // Visual Components const makeHeaderGroups = () => { - const theadGroupProps = _.splitProps(getTheadGroupProps(finalState, undefined, undefined, this)) - const theadGroupTrProps = _.splitProps(getTheadGroupTrProps(finalState, undefined, undefined, this)) + const theadGroupProps = _.splitProps( + getTheadGroupProps(finalState, undefined, undefined, this) + ) + const theadGroupTrProps = _.splitProps( + getTheadGroupTrProps(finalState, undefined, undefined, this) + ) return ( { - 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 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 theadGroupThProps = _.splitProps( + getTheadGroupThProps(finalState, undefined, column, this) + ) + const columnHeaderProps = _.splitProps( + column.getHeaderProps(finalState, undefined, column, this) + ) const classes = [ column.headerClassName, @@ -241,9 +268,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { return ( { - const theadProps = _.splitProps(getTheadProps(finalState, undefined, undefined, this)) - const theadTrProps = _.splitProps(getTheadTrProps(finalState, undefined, undefined, this)) + const theadProps = _.splitProps( + getTheadProps(finalState, undefined, undefined, this) + ) + const theadTrProps = _.splitProps( + getTheadTrProps(finalState, undefined, undefined, this) + ) return ( { 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 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, @@ -308,14 +351,13 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { } const isResizable = _.getFirstDefined(column.resizable, resizable, false) - const resizer = isResizable ? ( - this.resizeColumnStart(column, e, false)} - onTouchStart={e => this.resizeColumnStart(column, e, true)} - - {...resizerProps} - /> - ) : null + const resizer = isResizable + ? this.resizeColumnStart(column, e, false)} + onTouchStart={e => this.resizeColumnStart(column, e, true)} + {...resizerProps} + /> + : null const isSortable = _.getFirstDefined(column.sortable, sortable, false) @@ -328,7 +370,9 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { sort ? (sort.desc ? '-sort-desc' : '-sort-asc') : '', isSortable && '-cursor-pointer', !show && '-hidden', - pivotBy && pivotBy.slice(0, -1).includes(column.id) && 'rt-header-pivot' + pivotBy && + pivotBy.slice(0, -1).includes(column.id) && + 'rt-header-pivot' )} style={{ ...styles, @@ -336,12 +380,12 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { width: `${width}px`, maxWidth: `${maxWidth}px` }} - toggleSort={(e) => { + toggleSort={e => { isSortable && this.sortColumn(column, e.shiftKey) }} {...rest} > -
+
{_.normalizeComponent(column.Header, { data: sortedData, column: column @@ -353,8 +397,12 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { } const makeFilters = () => { - const theadFilterProps = _.splitProps(getTheadFilterProps(finalState, undefined, undefined, this)) - const theadFilterTrProps = _.splitProps(getTheadFilterTrProps(finalState, undefined, undefined, this)) + const theadFilterProps = _.splitProps( + getTheadFilterProps(finalState, undefined, undefined, this) + ) + const theadFilterTrProps = _.splitProps( + getTheadFilterTrProps(finalState, undefined, undefined, this) + ) return ( { 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 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, @@ -403,14 +463,16 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const ResolvedFilterComponent = column.Filter || FilterComponent - const isFilterable = _.getFirstDefined(column.filterable, filterable, false) + const isFilterable = _.getFirstDefined( + column.filterable, + filterable, + false + ) return ( - {isFilterable ? ( - _.normalizeComponent(ResolvedFilterComponent, - { - column, - filter, - onChange: (value) => (this.filterColumn(column, value)) - }, - defaultProps.column.Filter - ) - ) : null} + {isFilterable + ? _.normalizeComponent( + ResolvedFilterComponent, + { + column, + filter, + onChange: value => this.filterColumn(column, value) + }, + defaultProps.column.Filter + ) + : null} ) } @@ -447,12 +510,11 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { } const isExpanded = _.get(expanded, rowInfo.nestingPath) const trGroupProps = getTrGroupProps(finalState, rowInfo, undefined, this) - const trProps = _.splitProps(getTrProps(finalState, rowInfo, undefined, this)) + const trProps = _.splitProps( + getTrProps(finalState, rowInfo, undefined, this) + ) return ( - + {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 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, @@ -484,7 +560,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const cellInfo = { ...rowInfo, isExpanded, - column: {...column}, + column: { ...column }, value: rowInfo.row[column.id], pivoted: column.pivoted, expander: column.expander, @@ -504,7 +580,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { let isBranch let isPreview - const onExpanderClick = (e) => { + const onExpanderClick = e => { let newExpanded = _.clone(expanded) if (isExpanded) { newExpanded = _.set(newExpanded, cellInfo.nestingPath, false) @@ -512,29 +588,41 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { newExpanded = _.set(newExpanded, cellInfo.nestingPath, {}) } - return this.setStateWithData({ - expanded: newExpanded - }, () => { - onExpandedChange && onExpandedChange(newExpanded, cellInfo.nestingPath, e) - }) + return this.setStateWithData( + { + expanded: newExpanded + }, + () => { + onExpandedChange && + onExpandedChange(newExpanded, cellInfo.nestingPath, e) + } + ) } // Default to a standard cell - let resolvedCell = _.normalizeComponent(column.Cell, cellInfo, value) + 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 => ( -
- - -
- ) - ) - const ResolvedPivotComponent = column.Pivot || DefaultResolvedPivotComponent + const ResolvedAggregatedComponent = + column.Aggregated || + (!column.aggregate ? AggregatedComponent : column.Cell) + const ResolvedExpanderComponent = + column.Expander || ExpanderComponent + const ResolvedPivotValueComponent = + column.PivotValue || PivotValueComponent + const DefaultResolvedPivotComponent = + PivotComponent || + (props => +
+ + +
) + const ResolvedPivotComponent = + column.Pivot || DefaultResolvedPivotComponent // Is this cell expandable? if (cellInfo.pivoted || cellInfo.expander) { @@ -556,30 +644,47 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { if (cellInfo.pivoted) { // Is this column a branch? - isBranch = rowInfo.row[pivotIDKey] === column.id && - cellInfo.subRows + 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 + 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]) + resolvedCell = _.normalizeComponent( + ResolvedPivotComponent, + { + ...cellInfo, + value: row[pivotValKey] + }, + row[pivotValKey] + ) } else if (isPreview) { // Show the pivot preview - resolvedCell = _.normalizeComponent(ResolvedAggregatedComponent, cellInfo, value) + resolvedCell = _.normalizeComponent( + ResolvedAggregatedComponent, + cellInfo, + value + ) } else { resolvedCell = null } } else if (cellInfo.aggregated) { - resolvedCell = _.normalizeComponent(ResolvedAggregatedComponent, cellInfo, value) + resolvedCell = _.normalizeComponent( + ResolvedAggregatedComponent, + cellInfo, + value + ) } if (cellInfo.expander) { - resolvedCell = _.normalizeComponent(ResolvedExpanderComponent, cellInfo, row[pivotValKey]) + resolvedCell = _.normalizeComponent( + ResolvedExpanderComponent, + cellInfo, + row[pivotValKey] + ) if (pivotBy) { if (cellInfo.groupedByPivot) { resolvedCell = null @@ -614,24 +719,31 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) })}
- {( - rowInfo.subRows && + {rowInfo.subRows && isExpanded && - rowInfo.subRows.map((d, i) => makePageRow(d, i, rowInfo.nestingPath)) - )} - {SubComponent && !rowInfo.subRows && isExpanded && SubComponent(rowInfo)} + rowInfo.subRows.map((d, i) => + makePageRow(d, i, rowInfo.nestingPath) + )} + {SubComponent && + !rowInfo.subRows && + isExpanded && + SubComponent(rowInfo)}
) } const makePadRow = (row, i) => { - const trGroupProps = getTrGroupProps(finalState, undefined, undefined, this) - const trProps = _.splitProps(getTrProps(finalState, undefined, undefined, this)) + const trGroupProps = getTrGroupProps( + finalState, + undefined, + undefined, + this + ) + const trProps = _.splitProps( + getTrProps(finalState, undefined, undefined, this) + ) return ( - + { const resizedCol = resized.find(x => x.id === column.id) || {} - const show = typeof column.show === 'function' ? column.show() : column.show - let width = _.getFirstDefined(resizedCol.value, column.width, column.minWidth) + const show = typeof column.show === 'function' + ? column.show() + : column.show + let width = _.getFirstDefined( + resizedCol.value, + column.width, + column.minWidth + ) let flex = width - let 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)) + let 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, @@ -670,10 +796,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { return ( -   - + /> ) } const makeColumnFooters = () => { const tFootProps = getTfootProps(finalState, undefined, undefined, this) - const tFootTrProps = _.splitProps(getTfootTrProps(finalState, undefined, undefined, this)) + const tFootTrProps = _.splitProps( + getTfootTrProps(finalState, undefined, undefined, this) + ) return ( @@ -714,12 +835,28 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { 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 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, @@ -738,10 +875,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { return ( { - const paginationProps = _.splitProps(getPaginationProps(finalState, undefined, undefined, this)) - return + const paginationProps = _.splitProps( + getPaginationProps(finalState, undefined, undefined, this) + ) + return ( + + ) } - 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 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) const resizerProps = getResizerProps(finalState, undefined, undefined, this) @@ -786,11 +930,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const pagination = makePagination() return (
{showPagination && showPaginationTop - ?
- {pagination} -
+ ?
+ {pagination} +
: null} {showPagination && showPaginationBottom - ?
- {pagination} -
+ ?
+ {pagination} +
: null} - {!pageRows.length && ( - + {!pageRows.length && + {_.normalizeComponent(noDataText)} - - )} + } class extends Base { - componentWillMount () { - this.setStateWithData(this.getDataModel(this.getResolvedState())) - } - - componentDidMount () { - this.fireFetchData() - } - - componentWillReceiveProps (nextProps, nextState) { - const oldState = this.getResolvedState() - const newState = this.getResolvedState(nextProps, nextState) - - // Do a deep compare of new and old `defaultOption` and - // if they are different reset `option = defaultOption` - const defaultableOptions = ['sorted', 'filtered', 'resized', 'expanded'] - defaultableOptions.forEach(x => { - const defaultName = `default${x.charAt(0).toUpperCase() + x.slice(1)}` - if (JSON.stringify(oldState[defaultName]) !== JSON.stringify(newState[defaultName])) { - newState[x] = newState[defaultName] - } - }) - - // If they change these table options, we need to reset defaults - // or else we could get into a state where the user has changed the UI - // and then disabled the ability to change it back. - // e.g. If `filterable` has changed, set `filtered = defaultFiltered` - const resettableOptions = ['sortable', 'filterable', 'resizable'] - resettableOptions.forEach(x => { - if (oldState[x] !== newState[x]) { - const baseName = x.replace('able', '') - const optionName = `${baseName}ed` - const defaultName = `default${optionName.charAt(0).toUpperCase() + optionName.slice(1)}` - newState[optionName] = newState[defaultName] - } - }) - - // Props that trigger a data update - if ( - oldState.data !== newState.data || - oldState.columns !== newState.columns || - oldState.pivotBy !== newState.pivotBy || - oldState.sorted !== newState.sorted || - oldState.filtered !== newState.filtered - ) { - this.setStateWithData(this.getDataModel(newState)) +export default Base => + class extends Base { + componentWillMount () { + this.setStateWithData(this.getDataModel(this.getResolvedState())) } - } - setStateWithData (newState, cb) { - const oldState = this.getResolvedState() - const newResolvedState = this.getResolvedState({}, newState) - const {freezeWhenExpanded} = newResolvedState + componentDidMount () { + this.fireFetchData() + } - // Default to unfrozen state - newResolvedState.frozen = false + componentWillReceiveProps (nextProps, nextState) { + const oldState = this.getResolvedState() + const newState = this.getResolvedState(nextProps, nextState) - // If freezeWhenExpanded is set, check for frozen conditions - if (freezeWhenExpanded) { - // if any rows are expanded, freeze the existing data and sorting - const keys = Object.keys(newResolvedState.expanded) - for (var i = 0; i < keys.length; i++) { - if (newResolvedState.expanded[keys[i]]) { - newResolvedState.frozen = true - break + // Do a deep compare of new and old `defaultOption` and + // if they are different reset `option = defaultOption` + const defaultableOptions = ['sorted', 'filtered', 'resized', 'expanded'] + defaultableOptions.forEach(x => { + const defaultName = `default${x.charAt(0).toUpperCase() + x.slice(1)}` + if ( + JSON.stringify(oldState[defaultName]) !== + JSON.stringify(newState[defaultName]) + ) { + newState[x] = newState[defaultName] + } + }) + + // If they change these table options, we need to reset defaults + // or else we could get into a state where the user has changed the UI + // and then disabled the ability to change it back. + // e.g. If `filterable` has changed, set `filtered = defaultFiltered` + const resettableOptions = ['sortable', 'filterable', 'resizable'] + resettableOptions.forEach(x => { + if (oldState[x] !== newState[x]) { + const baseName = x.replace('able', '') + const optionName = `${baseName}ed` + const defaultName = `default${optionName.charAt(0).toUpperCase() + + optionName.slice(1)}` + newState[optionName] = newState[defaultName] + } + }) + + // Props that trigger a data update + if ( + oldState.data !== newState.data || + oldState.columns !== newState.columns || + oldState.pivotBy !== newState.pivotBy || + oldState.sorted !== newState.sorted || + oldState.filtered !== newState.filtered + ) { + this.setStateWithData(this.getDataModel(newState)) + } + } + + setStateWithData (newState, cb) { + const oldState = this.getResolvedState() + const newResolvedState = this.getResolvedState({}, newState) + const { freezeWhenExpanded } = newResolvedState + + // Default to unfrozen state + newResolvedState.frozen = false + + // If freezeWhenExpanded is set, check for frozen conditions + if (freezeWhenExpanded) { + // if any rows are expanded, freeze the existing data and sorting + const keys = Object.keys(newResolvedState.expanded) + for (var i = 0; i < keys.length; i++) { + if (newResolvedState.expanded[keys[i]]) { + newResolvedState.frozen = true + break + } } } - } - // If the data isn't frozen and either the data or - // sorting model has changed, update the data - if ( - (oldState.frozen && !newResolvedState.frozen) || - oldState.sorted !== newResolvedState.sorted || - oldState.filtered !== newResolvedState.filtered || - oldState.showFilters !== newResolvedState.showFilters || - (!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData) - ) { - // Handle collapseOnsortedChange & collapseOnDataChange + // If the data isn't frozen and either the data or + // sorting model has changed, update the data if ( - (oldState.sorted !== newResolvedState.sorted && this.props.collapseOnSortingChange) || - (oldState.filtered !== newResolvedState.filtered) || - (oldState.showFilters !== newResolvedState.showFilters) || - (!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData && this.props.collapseOnDataChange) + (oldState.frozen && !newResolvedState.frozen) || + oldState.sorted !== newResolvedState.sorted || + oldState.filtered !== newResolvedState.filtered || + oldState.showFilters !== newResolvedState.showFilters || + (!newResolvedState.frozen && + oldState.resolvedData !== newResolvedState.resolvedData) ) { - newResolvedState.expanded = {} + // Handle collapseOnsortedChange & collapseOnDataChange + if ( + (oldState.sorted !== newResolvedState.sorted && + this.props.collapseOnSortingChange) || + oldState.filtered !== newResolvedState.filtered || + oldState.showFilters !== newResolvedState.showFilters || + (!newResolvedState.frozen && + oldState.resolvedData !== newResolvedState.resolvedData && + this.props.collapseOnDataChange) + ) { + newResolvedState.expanded = {} + } + + Object.assign(newResolvedState, this.getSortedData(newResolvedState)) } - Object.assign(newResolvedState, this.getSortedData(newResolvedState)) - } + // Calculate pageSize all the time + if (newResolvedState.sortedData) { + newResolvedState.pages = newResolvedState.manual + ? newResolvedState.pages + : Math.ceil( + newResolvedState.sortedData.length / newResolvedState.pageSize + ) + newResolvedState.page = Math.max( + newResolvedState.page >= newResolvedState.pages + ? newResolvedState.pages - 1 + : newResolvedState.page, + 0 + ) + } - // Calculate pageSize all the time - if (newResolvedState.sortedData) { - newResolvedState.pages = newResolvedState.manual ? newResolvedState.pages : Math.ceil(newResolvedState.sortedData.length / newResolvedState.pageSize) - newResolvedState.page = Math.max(newResolvedState.page >= newResolvedState.pages ? newResolvedState.pages - 1 : newResolvedState.page, 0) + return this.setState(newResolvedState, cb) } - - return this.setState(newResolvedState, cb) } -} diff --git a/src/methods.js b/src/methods.js index 6c49b6e..48f227d 100644 --- a/src/methods.js +++ b/src/methods.js @@ -1,637 +1,697 @@ import React from 'react' import _ from './utils' -export default Base => class extends Base { - getResolvedState (props, state) { - const resolvedState = { - ..._.compactObject(this.state), - ..._.compactObject(this.props), - ..._.compactObject(state), - ..._.compactObject(props) - } - return resolvedState - } - - getDataModel (newState) { - const { - columns, - pivotBy = [], - data, - pivotIDKey, - pivotValKey, - subRowsKey, - aggregatedKey, - nestingLevelKey, - originalKey, - indexKey, - groupedByPivotKey, - SubComponent - } = newState - - // Determine Header Groups - let hasHeaderGroups = false - columns.forEach(column => { - if (column.columns) { - hasHeaderGroups = true +export default Base => + class extends Base { + getResolvedState (props, state) { + const resolvedState = { + ..._.compactObject(this.state), + ..._.compactObject(this.props), + ..._.compactObject(state), + ..._.compactObject(props), } - }) - - let columnsWithExpander = [...columns] - - let expanderColumn = columns.find(col => col.expander || (col.columns && col.columns.some(col2 => col2.expander))) - // The actual expander might be in the columns field of a group column - if (expanderColumn && !expanderColumn.expander) { - expanderColumn = expanderColumn.columns.find(col => col.expander) + return resolvedState } - // If we have SubComponent's we need to make sure we have an expander column - if (SubComponent && !expanderColumn) { - expanderColumn = {expander: true} - columnsWithExpander = [expanderColumn, ...columnsWithExpander] - } + getDataModel (newState) { + const { + columns, + pivotBy = [], + data, + pivotIDKey, + pivotValKey, + subRowsKey, + aggregatedKey, + nestingLevelKey, + originalKey, + indexKey, + groupedByPivotKey, + SubComponent, + } = newState - const makeDecoratedColumn = (column) => { - let dcol - if (column.expander) { - dcol = { - ...this.props.column, - ...this.props.expanderDefaults, - ...column - } - } else { - dcol = { - ...this.props.column, - ...column + // Determine Header Groups + let hasHeaderGroups = false + columns.forEach(column => { + if (column.columns) { + hasHeaderGroups = true } + }) + + let columnsWithExpander = [...columns] + + let expanderColumn = columns.find( + col => + col.expander || + (col.columns && col.columns.some(col2 => col2.expander)) + ) + // The actual expander might be in the columns field of a group column + if (expanderColumn && !expanderColumn.expander) { + expanderColumn = expanderColumn.columns.find(col => col.expander) } - if (typeof dcol.accessor === 'string') { - dcol.id = dcol.id || dcol.accessor - const accessorString = dcol.accessor - dcol.accessor = row => _.get(row, accessorString) + // If we have SubComponent's we need to make sure we have an expander column + if (SubComponent && !expanderColumn) { + expanderColumn = { expander: true } + columnsWithExpander = [expanderColumn, ...columnsWithExpander] + } + + const makeDecoratedColumn = column => { + let dcol + if (column.expander) { + dcol = { + ...this.props.column, + ...this.props.expanderDefaults, + ...column, + } + } else { + dcol = { + ...this.props.column, + ...column, + } + } + + if (typeof dcol.accessor === 'string') { + dcol.id = dcol.id || dcol.accessor + const accessorString = dcol.accessor + dcol.accessor = row => _.get(row, accessorString) + return dcol + } + + if (dcol.accessor && !dcol.id) { + console.warn(dcol) + throw new Error( + 'A column id is required if using a non-string accessor for column above.' + ) + } + + if (!dcol.accessor) { + dcol.accessor = d => undefined + } + + // Ensure minWidth is not greater than maxWidth if set + if (dcol.maxWidth < dcol.minWidth) { + dcol.minWidth = dcol.maxWidth + } + return dcol } - if (dcol.accessor && !dcol.id) { - console.warn(dcol) - throw new Error('A column id is required if using a non-string accessor for column above.') + // Decorate the columns + const decorateAndAddToAll = col => { + const decoratedColumn = makeDecoratedColumn(col) + allDecoratedColumns.push(decoratedColumn) + return decoratedColumn } - - if (!dcol.accessor) { - dcol.accessor = d => undefined - } - - // Ensure minWidth is not greater than maxWidth if set - if (dcol.maxWidth < dcol.minWidth) { - dcol.minWidth = dcol.maxWidth - } - - return dcol - } - - // Decorate the columns - const decorateAndAddToAll = (col) => { - const decoratedColumn = makeDecoratedColumn(col) - allDecoratedColumns.push(decoratedColumn) - return decoratedColumn - } - let allDecoratedColumns = [] - const decoratedColumns = columnsWithExpander.map((column, i) => { - if (column.columns) { - return { - ...column, - columns: column.columns.map(decorateAndAddToAll) - } - } else { - return decorateAndAddToAll(column) - } - }) - - // Build the visible columns, headers and flat column list - let visibleColumns = decoratedColumns.slice() - let allVisibleColumns = [] - - visibleColumns = visibleColumns.map((column, i) => { - if (column.columns) { - const visibleSubColumns = column.columns.filter(d => pivotBy.indexOf(d.id) > -1 ? false : _.getFirstDefined(d.show, true)) - return { - ...column, - columns: visibleSubColumns - } - } - return column - }) - - visibleColumns = visibleColumns.filter(column => { - return column.columns ? column.columns.length : pivotBy.indexOf(column.id) > -1 ? false : _.getFirstDefined(column.show, true) - }) - - // Find any custom pivot location - const pivotIndex = visibleColumns.findIndex(col => col.pivot) - - // Handle Pivot Columns - if (pivotBy.length) { - // Retrieve the pivot columns in the correct pivot order - const pivotColumns = [] - pivotBy.forEach(pivotID => { - const found = allDecoratedColumns.find(d => d.id === pivotID) - if (found) { - pivotColumns.push(found) - } - }) - - let pivotColumnGroup = { - header: () => Group, - columns: pivotColumns.map(col => ({ - ...this.props.pivotDefaults, - ...col, - pivoted: true - })) - } - - // Place the pivotColumns back into the visibleColumns - if (pivotIndex >= 0) { - pivotColumnGroup = { - ...visibleColumns[pivotIndex], - ...pivotColumnGroup - } - visibleColumns.splice(pivotIndex, 1, pivotColumnGroup) - } else { - visibleColumns.unshift(pivotColumnGroup) - } - } - - // Build Header Groups - const headerGroups = [] - let currentSpan = [] - - // A convenience function to add a header and reset the currentSpan - const addHeader = (columns, column) => { - headerGroups.push({ - ...this.props.column, - ...column, - columns: columns - }) - currentSpan = [] - } - - // Build flast list of allVisibleColumns and HeaderGroups - visibleColumns.forEach((column, i) => { - if (column.columns) { - allVisibleColumns = allVisibleColumns.concat(column.columns) - if (currentSpan.length > 0) { - addHeader(currentSpan) - } - addHeader(column.columns, column) - return - } - allVisibleColumns.push(column) - currentSpan.push(column) - }) - if (hasHeaderGroups && currentSpan.length > 0) { - addHeader(currentSpan) - } - - // Access the data - const accessRow = (d, i, level = 0) => { - const row = { - [originalKey]: d, - [indexKey]: i, - [subRowsKey]: d[subRowsKey], - [nestingLevelKey]: level - } - allDecoratedColumns.forEach(column => { - if (column.expander) return - row[column.id] = column.accessor(d) - }) - if (row[subRowsKey]) { - row[subRowsKey] = row[subRowsKey].map((d, i) => accessRow(d, i, level + 1)) - } - return row - } - let resolvedData = data.map((d, i) => accessRow(d, i)) - - // If pivoting, recursively group the data - const aggregate = (rows) => { - const aggregationValues = {} - aggregatingColumns.forEach(column => { - const values = rows.map(d => d[column.id]) - aggregationValues[column.id] = column.aggregate(values, rows) - }) - return aggregationValues - } - - // TODO: Make it possible to fabricate nested rows without pivoting - const aggregatingColumns = allVisibleColumns.filter(d => !d.expander && d.aggregate) - if (pivotBy.length) { - const groupRecursively = (rows, keys, i = 0) => { - // This is the last level, just return the rows - if (i === keys.length) { - return rows - } - // Group the rows together for this level - let groupedRows = Object.entries( - _.groupBy(rows, keys[i])) - .map(([key, value]) => { + let allDecoratedColumns = [] + const decoratedColumns = columnsWithExpander.map((column, i) => { + if (column.columns) { return { - [pivotIDKey]: keys[i], - [pivotValKey]: key, - [keys[i]]: key, - [subRowsKey]: value, - [nestingLevelKey]: i, - [groupedByPivotKey]: true - } - }) - // Recurse into the subRows - groupedRows = groupedRows.map(rowGroup => { - let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1) - return { - ...rowGroup, - [subRowsKey]: subRows, - [aggregatedKey]: true, - ...aggregate(subRows) - } - }) - return groupedRows - } - resolvedData = groupRecursively(resolvedData, pivotBy) - } - - return { - ...newState, - resolvedData, - allVisibleColumns, - headerGroups, - allDecoratedColumns, - hasHeaderGroups - } - } - - getSortedData (resolvedState) { - const { - manual, - sorted, - filtered, - defaultFilterMethod, - resolvedData, - allVisibleColumns, - allDecoratedColumns - } = resolvedState - - const sortMethodsByColumnID = {} - - allDecoratedColumns - .filter(col => col.sortMethod) - .forEach(col => { - sortMethodsByColumnID[col.id] = col.sortMethod - }) - - // Resolve the data from either manual data or sorted data - return { - sortedData: manual ? resolvedData : this.sortData( - this.filterData( - resolvedData, - filtered, - defaultFilterMethod, - allVisibleColumns - ), - sorted, - sortMethodsByColumnID - ) - } - } - - fireFetchData () { - this.props.onFetchData(this.getResolvedState(), this) - } - - getPropOrState (key) { - return _.getFirstDefined(this.props[key], this.state[key]) - } - - getStateOrProp (key) { - return _.getFirstDefined(this.state[key], this.props[key]) - } - - filterData (data, filtered, defaultFilterMethod, allVisibleColumns) { - let filteredData = data - - if (filtered.length) { - filteredData = filtered.reduce( - (filteredSoFar, nextFilter) => { - return filteredSoFar.filter( - (row) => { - let column - - column = allVisibleColumns.find(x => x.id === nextFilter.id) - - // Don't filter hidden columns or columns that have had their filters disabled - if (!column || column.filterable === false) { - return true - } - - const filterMethod = column.filterMethod || defaultFilterMethod - - return filterMethod(nextFilter, row, column) - }) - } - , filteredData - ) - - // Apply the filter to the subrows if we are pivoting, and then - // filter any rows without subcolumns because it would be strange to show - filteredData = filteredData.map(row => { - if (!row[this.props.subRowsKey]) { - return row - } - return { - ...row, - [this.props.subRowsKey]: this.filterData(row[this.props.subRowsKey], filtered, defaultFilterMethod, allVisibleColumns) - } - }).filter(row => { - if (!row[this.props.subRowsKey]) { - return true - } - return row[this.props.subRowsKey].length > 0 - }) - } - - return filteredData - } - - sortData (data, sorted, sortMethodsByColumnID = {}) { - if (!sorted.length) { - return data - } - - const sortedData = (this.props.orderByMethod || _.orderBy)( - data, - sorted.map(sort => { - // Support custom sorting methods for each column - if (sortMethodsByColumnID[sort.id]) { - return (a, b) => { - return sortMethodsByColumnID[sort.id](a[sort.id], b[sort.id]) - } - } - return (a, b) => { - return this.props.defaultSortMethod(a[sort.id], b[sort.id]) - } - }), - sorted.map(d => !d.desc), - this.props.indexKey - ) - - sortedData.forEach(row => { - if (!row[this.props.subRowsKey]) { - return - } - row[this.props.subRowsKey] = this.sortData(row[this.props.subRowsKey], sorted, sortMethodsByColumnID) - }) - - return sortedData - } - - getMinRows () { - return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize')) - } - - // User actions - onPageChange (page) { - const {onPageChange, collapseOnPageChange} = this.props - - const newState = {page} - if (collapseOnPageChange) { - newState.expanded = {} - } - this.setStateWithData(newState, () => { - onPageChange && onPageChange(page) - this.fireFetchData() - }) - } - - onPageSizeChange (newPageSize) { - const {onPageSizeChange} = this.props - const {pageSize, page} = this.getResolvedState() - - // Normalize the page to display - const currentRow = pageSize * page - const newPage = Math.floor(currentRow / newPageSize) - - this.setStateWithData({ - pageSize: newPageSize, - page: newPage - }, () => { - onPageSizeChange && onPageSizeChange(newPageSize, newPage) - this.fireFetchData() - }) - } - - sortColumn (column, additive) { - const {sorted, skipNextSort} = this.getResolvedState() - - // we can't stop event propagation from the column resize move handlers - // attached to the document because of react's synthetic events - // so we have to prevent the sort function from actually sorting - // if we click on the column resize element within a header. - if (skipNextSort) { - this.setStateWithData({ - skipNextSort: false - }) - return - } - - const {onSortedChange} = this.props - - let newSorted = _.clone(sorted || []).map(d => { - d.desc = _.isSortingDesc(d) - return d - }) - if (!_.isArray(column)) { - // Single-Sort - const existingIndex = newSorted.findIndex(d => d.id === column.id) - if (existingIndex > -1) { - const existing = newSorted[existingIndex] - if (existing.desc) { - if (additive) { - newSorted.splice(existingIndex, 1) - } else { - existing.desc = false - newSorted = [existing] + ...column, + columns: column.columns.map(decorateAndAddToAll), } } else { - existing.desc = true - if (!additive) { - newSorted = [existing] + return decorateAndAddToAll(column) + } + }) + + // Build the visible columns, headers and flat column list + let visibleColumns = decoratedColumns.slice() + let allVisibleColumns = [] + + visibleColumns = visibleColumns.map((column, i) => { + if (column.columns) { + const visibleSubColumns = column.columns.filter( + d => + pivotBy.indexOf(d.id) > -1 + ? false + : _.getFirstDefined(d.show, true) + ) + return { + ...column, + columns: visibleSubColumns, } } - } else { - if (additive) { - newSorted.push({ - id: column.id, - desc: false + return column + }) + + visibleColumns = visibleColumns.filter(column => { + return column.columns + ? column.columns.length + : pivotBy.indexOf(column.id) > -1 + ? false + : _.getFirstDefined(column.show, true) + }) + + // Find any custom pivot location + const pivotIndex = visibleColumns.findIndex(col => col.pivot) + + // Handle Pivot Columns + if (pivotBy.length) { + // Retrieve the pivot columns in the correct pivot order + const pivotColumns = [] + pivotBy.forEach(pivotID => { + const found = allDecoratedColumns.find(d => d.id === pivotID) + if (found) { + pivotColumns.push(found) + } + }) + + let pivotColumnGroup = { + header: () => Group, + columns: pivotColumns.map(col => ({ + ...this.props.pivotDefaults, + ...col, + pivoted: true, + })), + } + + // Place the pivotColumns back into the visibleColumns + if (pivotIndex >= 0) { + pivotColumnGroup = { + ...visibleColumns[pivotIndex], + ...pivotColumnGroup, + } + visibleColumns.splice(pivotIndex, 1, pivotColumnGroup) + } else { + visibleColumns.unshift(pivotColumnGroup) + } + } + + // Build Header Groups + const headerGroups = [] + let currentSpan = [] + + // A convenience function to add a header and reset the currentSpan + const addHeader = (columns, column) => { + headerGroups.push({ + ...this.props.column, + ...column, + columns: columns, + }) + currentSpan = [] + } + + // Build flast list of allVisibleColumns and HeaderGroups + visibleColumns.forEach((column, i) => { + if (column.columns) { + allVisibleColumns = allVisibleColumns.concat(column.columns) + if (currentSpan.length > 0) { + addHeader(currentSpan) + } + addHeader(column.columns, column) + return + } + allVisibleColumns.push(column) + currentSpan.push(column) + }) + if (hasHeaderGroups && currentSpan.length > 0) { + addHeader(currentSpan) + } + + // Access the data + const accessRow = (d, i, level = 0) => { + const row = { + [originalKey]: d, + [indexKey]: i, + [subRowsKey]: d[subRowsKey], + [nestingLevelKey]: level, + } + allDecoratedColumns.forEach(column => { + if (column.expander) return + row[column.id] = column.accessor(d) + }) + if (row[subRowsKey]) { + row[subRowsKey] = row[subRowsKey].map((d, i) => + accessRow(d, i, level + 1) + ) + } + return row + } + let resolvedData = data.map((d, i) => accessRow(d, i)) + + // If pivoting, recursively group the data + const aggregate = rows => { + const aggregationValues = {} + aggregatingColumns.forEach(column => { + const values = rows.map(d => d[column.id]) + aggregationValues[column.id] = column.aggregate(values, rows) + }) + return aggregationValues + } + + // TODO: Make it possible to fabricate nested rows without pivoting + const aggregatingColumns = allVisibleColumns.filter( + d => !d.expander && d.aggregate + ) + if (pivotBy.length) { + const groupRecursively = (rows, keys, i = 0) => { + // This is the last level, just return the rows + if (i === keys.length) { + return rows + } + // Group the rows together for this level + let groupedRows = Object.entries( + _.groupBy(rows, keys[i]) + ).map(([key, value]) => { + return { + [pivotIDKey]: keys[i], + [pivotValKey]: key, + [keys[i]]: key, + [subRowsKey]: value, + [nestingLevelKey]: i, + [groupedByPivotKey]: true, + } }) - } else { - newSorted = [{ - id: column.id, - desc: false - }] + // Recurse into the subRows + groupedRows = groupedRows.map(rowGroup => { + let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1) + return { + ...rowGroup, + [subRowsKey]: subRows, + [aggregatedKey]: true, + ...aggregate(subRows), + } + }) + return groupedRows } + resolvedData = groupRecursively(resolvedData, pivotBy) } - } else { - // Multi-Sort - const existingIndex = newSorted.findIndex(d => d.id === column[0].id) - // Existing Sorted Column - if (existingIndex > -1) { - const existing = newSorted[existingIndex] - if (existing.desc) { + + return { + ...newState, + resolvedData, + allVisibleColumns, + headerGroups, + allDecoratedColumns, + hasHeaderGroups, + } + } + + getSortedData (resolvedState) { + const { + manual, + sorted, + filtered, + defaultFilterMethod, + resolvedData, + allVisibleColumns, + allDecoratedColumns, + } = resolvedState + + const sortMethodsByColumnID = {} + + allDecoratedColumns.filter(col => col.sortMethod).forEach(col => { + sortMethodsByColumnID[col.id] = col.sortMethod + }) + + // Resolve the data from either manual data or sorted data + return { + sortedData: manual + ? resolvedData + : this.sortData( + this.filterData( + resolvedData, + filtered, + defaultFilterMethod, + allVisibleColumns + ), + sorted, + sortMethodsByColumnID + ), + } + } + + fireFetchData () { + this.props.onFetchData(this.getResolvedState(), this) + } + + getPropOrState (key) { + return _.getFirstDefined(this.props[key], this.state[key]) + } + + getStateOrProp (key) { + return _.getFirstDefined(this.state[key], this.props[key]) + } + + filterData (data, filtered, defaultFilterMethod, allVisibleColumns) { + let filteredData = data + + if (filtered.length) { + filteredData = filtered.reduce((filteredSoFar, nextFilter) => { + return filteredSoFar.filter(row => { + let column + + column = allVisibleColumns.find(x => x.id === nextFilter.id) + + // Don't filter hidden columns or columns that have had their filters disabled + if (!column || column.filterable === false) { + return true + } + + const filterMethod = column.filterMethod || defaultFilterMethod + + return filterMethod(nextFilter, row, column) + }) + }, filteredData) + + // Apply the filter to the subrows if we are pivoting, and then + // filter any rows without subcolumns because it would be strange to show + filteredData = filteredData + .map(row => { + if (!row[this.props.subRowsKey]) { + return row + } + return { + ...row, + [this.props.subRowsKey]: this.filterData( + row[this.props.subRowsKey], + filtered, + defaultFilterMethod, + allVisibleColumns + ), + } + }) + .filter(row => { + if (!row[this.props.subRowsKey]) { + return true + } + return row[this.props.subRowsKey].length > 0 + }) + } + + return filteredData + } + + sortData (data, sorted, sortMethodsByColumnID = {}) { + if (!sorted.length) { + return data + } + + const sortedData = (this.props.orderByMethod || _.orderBy)( + data, + sorted.map(sort => { + // Support custom sorting methods for each column + if (sortMethodsByColumnID[sort.id]) { + return (a, b) => { + return sortMethodsByColumnID[sort.id](a[sort.id], b[sort.id]) + } + } + return (a, b) => { + return this.props.defaultSortMethod(a[sort.id], b[sort.id]) + } + }), + sorted.map(d => !d.desc), + this.props.indexKey + ) + + sortedData.forEach(row => { + if (!row[this.props.subRowsKey]) { + return + } + row[this.props.subRowsKey] = this.sortData( + row[this.props.subRowsKey], + sorted, + sortMethodsByColumnID + ) + }) + + return sortedData + } + + getMinRows () { + return _.getFirstDefined( + this.props.minRows, + this.getStateOrProp('pageSize') + ) + } + + // User actions + onPageChange (page) { + const { onPageChange, collapseOnPageChange } = this.props + + const newState = { page } + if (collapseOnPageChange) { + newState.expanded = {} + } + this.setStateWithData(newState, () => { + onPageChange && onPageChange(page) + this.fireFetchData() + }) + } + + onPageSizeChange (newPageSize) { + const { onPageSizeChange } = this.props + const { pageSize, page } = this.getResolvedState() + + // Normalize the page to display + const currentRow = pageSize * page + const newPage = Math.floor(currentRow / newPageSize) + + this.setStateWithData( + { + pageSize: newPageSize, + page: newPage, + }, + () => { + onPageSizeChange && onPageSizeChange(newPageSize, newPage) + this.fireFetchData() + } + ) + } + + sortColumn (column, additive) { + const { sorted, skipNextSort, defaultSortDesc } = this.getResolvedState() + + const firstSortDirection = column.hasOwnProperty('defaultSortDesc') + ? column.defaultSortDesc + : defaultSortDesc + const secondSortDirection = !firstSortDirection + + // we can't stop event propagation from the column resize move handlers + // attached to the document because of react's synthetic events + // so we have to prevent the sort function from actually sorting + // if we click on the column resize element within a header. + if (skipNextSort) { + this.setStateWithData({ + skipNextSort: false, + }) + return + } + + const { onSortedChange } = this.props + + let newSorted = _.clone(sorted || []).map(d => { + d.desc = _.isSortingDesc(d) + return d + }) + if (!_.isArray(column)) { + // Single-Sort + const existingIndex = newSorted.findIndex(d => d.id === column.id) + if (existingIndex > -1) { + const existing = newSorted[existingIndex] + if (existing.desc === secondSortDirection) { + if (additive) { + newSorted.splice(existingIndex, 1) + } else { + existing.desc = firstSortDirection + newSorted = [existing] + } + } else { + existing.desc = secondSortDirection + if (!additive) { + newSorted = [existing] + } + } + } else { if (additive) { - newSorted.splice(existingIndex, column.length) + newSorted.push({ + id: column.id, + desc: firstSortDirection, + }) + } else { + newSorted = [ + { + id: column.id, + desc: firstSortDirection, + }, + ] + } + } + } else { + // Multi-Sort + const existingIndex = newSorted.findIndex(d => d.id === column[0].id) + // Existing Sorted Column + if (existingIndex > -1) { + const existing = newSorted[existingIndex] + if (existing.desc === secondSortDirection) { + if (additive) { + newSorted.splice(existingIndex, column.length) + } else { + column.forEach((d, i) => { + newSorted[existingIndex + i].desc = firstSortDirection + }) + } } else { column.forEach((d, i) => { - newSorted[existingIndex + i].desc = false + newSorted[existingIndex + i].desc = secondSortDirection }) } + if (!additive) { + newSorted = newSorted.slice(existingIndex, column.length) + } } else { - column.forEach((d, i) => { - newSorted[existingIndex + i].desc = true - }) - } - if (!additive) { - newSorted = newSorted.slice(existingIndex, column.length) - } - } else { - // New Sort Column - if (additive) { - newSorted = newSorted.concat(column.map(d => ({ - id: d.id, - desc: false - }))) - } else { - newSorted = column.map(d => ({ - id: d.id, - desc: false - })) + // New Sort Column + if (additive) { + newSorted = newSorted.concat( + column.map(d => ({ + id: d.id, + desc: firstSortDirection, + })) + ) + } else { + newSorted = column.map(d => ({ + id: d.id, + desc: firstSortDirection, + })) + } } } + + this.setStateWithData( + { + page: (!sorted.length && newSorted.length) || !additive + ? 0 + : this.state.page, + sorted: newSorted, + }, + () => { + onSortedChange && onSortedChange(newSorted, column, additive) + this.fireFetchData() + } + ) } - this.setStateWithData({ - page: ((!sorted.length && newSorted.length) || !additive) ? 0 : this.state.page, - sorted: newSorted - }, () => { - onSortedChange && onSortedChange(newSorted, column, additive) - this.fireFetchData() - }) - } + filterColumn (column, value) { + const { filtered } = this.getResolvedState() + const { onFilteredChange } = this.props - filterColumn (column, value) { - const {filtered} = this.getResolvedState() - const {onFilteredChange} = this.props - - // Remove old filter first if it exists - const newFiltering = (filtered || []).filter(x => { - if (x.id !== column.id) { - return true - } - }) - - if (value !== '') { - newFiltering.push({ - id: column.id, - value: value + // Remove old filter first if it exists + const newFiltering = (filtered || []).filter(x => { + if (x.id !== column.id) { + return true + } }) - } - this.setStateWithData({ - filtered: newFiltering - }, () => { - onFilteredChange && onFilteredChange(newFiltering, column, value) - this.fireFetchData() - }) - } - - resizeColumnStart (column, event, isTouch) { - const parentWidth = event.target.parentElement.getBoundingClientRect().width - - let pageX - if (isTouch) { - pageX = event.changedTouches[0].pageX - } else { - pageX = event.pageX - } - - this.setStateWithData({ - currentlyResizing: { - id: column.id, - startX: pageX, - parentWidth: parentWidth + if (value !== '') { + newFiltering.push({ + id: column.id, + value: value, + }) } - }, () => { + + this.setStateWithData( + { + filtered: newFiltering, + }, + () => { + onFilteredChange && onFilteredChange(newFiltering, column, value) + this.fireFetchData() + } + ) + } + + resizeColumnStart (column, event, isTouch) { + const parentWidth = event.target.parentElement.getBoundingClientRect() + .width + + let pageX if (isTouch) { - document.addEventListener('touchmove', this.resizeColumnMoving) - document.addEventListener('touchcancel', this.resizeColumnEnd) - document.addEventListener('touchend', this.resizeColumnEnd) + pageX = event.changedTouches[0].pageX } else { - document.addEventListener('mousemove', this.resizeColumnMoving) - document.addEventListener('mouseup', this.resizeColumnEnd) - document.addEventListener('mouseleave', this.resizeColumnEnd) + pageX = event.pageX } - }) - } - resizeColumnEnd (event) { - let isTouch = event.type === 'touchend' || event.type === 'touchcancel' - - if (isTouch) { - document.removeEventListener('touchmove', this.resizeColumnMoving) - document.removeEventListener('touchcancel', this.resizeColumnEnd) - document.removeEventListener('touchend', this.resizeColumnEnd) + this.setStateWithData( + { + currentlyResizing: { + id: column.id, + startX: pageX, + parentWidth: parentWidth, + }, + }, + () => { + if (isTouch) { + document.addEventListener('touchmove', this.resizeColumnMoving) + document.addEventListener('touchcancel', this.resizeColumnEnd) + document.addEventListener('touchend', this.resizeColumnEnd) + } else { + document.addEventListener('mousemove', this.resizeColumnMoving) + document.addEventListener('mouseup', this.resizeColumnEnd) + document.addEventListener('mouseleave', this.resizeColumnEnd) + } + } + ) } - // If its a touch event clear the mouse one's as well because sometimes - // the mouseDown event gets called as well, but the mouseUp event doesn't - document.removeEventListener('mousemove', this.resizeColumnMoving) - document.removeEventListener('mouseup', this.resizeColumnEnd) - document.removeEventListener('mouseleave', this.resizeColumnEnd) + resizeColumnEnd (event) { + let isTouch = event.type === 'touchend' || event.type === 'touchcancel' - // The touch events don't propagate up to the sorting's onMouseDown event so - // no need to prevent it from happening or else the first click after a touch - // event resize will not sort the column. - if (!isTouch) { - this.setStateWithData({ - skipNextSort: true, - currentlyResizing: false + if (isTouch) { + document.removeEventListener('touchmove', this.resizeColumnMoving) + document.removeEventListener('touchcancel', this.resizeColumnEnd) + document.removeEventListener('touchend', this.resizeColumnEnd) + } + + // If its a touch event clear the mouse one's as well because sometimes + // the mouseDown event gets called as well, but the mouseUp event doesn't + document.removeEventListener('mousemove', this.resizeColumnMoving) + document.removeEventListener('mouseup', this.resizeColumnEnd) + document.removeEventListener('mouseleave', this.resizeColumnEnd) + + // The touch events don't propagate up to the sorting's onMouseDown event so + // no need to prevent it from happening or else the first click after a touch + // event resize will not sort the column. + if (!isTouch) { + this.setStateWithData({ + skipNextSort: true, + currentlyResizing: false, + }) + } + } + + resizeColumnMoving (event) { + const { onResizedChange } = this.props + const { resized, currentlyResizing } = this.getResolvedState() + + // Delete old value + const newResized = resized.filter(x => x.id !== currentlyResizing.id) + + let pageX + + if (event.type === 'touchmove') { + pageX = event.changedTouches[0].pageX + } else if (event.type === 'mousemove') { + pageX = event.pageX + } + + // Set the min size to 10 to account for margin and border or else the group headers don't line up correctly + const newWidth = Math.max( + currentlyResizing.parentWidth + pageX - currentlyResizing.startX, + 11 + ) + + newResized.push({ + id: currentlyResizing.id, + value: newWidth, }) + + this.setStateWithData( + { + resized: newResized, + }, + () => { + onResizedChange && onResizedChange(newResized, event) + } + ) } } - - resizeColumnMoving (event) { - const {onResizedChange} = this.props - const {resized, currentlyResizing} = this.getResolvedState() - - // Delete old value - const newResized = resized.filter(x => x.id !== currentlyResizing.id) - - let pageX - - if (event.type === 'touchmove') { - pageX = event.changedTouches[0].pageX - } else if (event.type === 'mousemove') { - pageX = event.pageX - } - - // Set the min size to 10 to account for margin and border or else the group headers don't line up correctly - const newWidth = Math.max(currentlyResizing.parentWidth + pageX - currentlyResizing.startX, 11) - - newResized.push({ - id: currentlyResizing.id, - value: newWidth - }) - - this.setStateWithData({ - resized: newResized - }, () => { - onResizedChange && onResizedChange(newResized, event) - }) - } -} diff --git a/src/pagination.js b/src/pagination.js index 6e2eaf8..12e70f3 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -3,9 +3,8 @@ import classnames from 'classnames' // // import _ from './utils' -const defaultButton = (props) => ( +const defaultButton = props => -) export default class ReactTablePagination extends Component { constructor (props) { @@ -16,12 +15,12 @@ export default class ReactTablePagination extends Component { this.applyPage = this.applyPage.bind(this) this.state = { - page: props.page + page: props.page, } } componentWillReceiveProps (nextProps) { - this.setState({page: nextProps.page}) + this.setState({ page: nextProps.page }) } getSafePage (page) { @@ -33,7 +32,7 @@ export default class ReactTablePagination extends Component { changePage (page) { page = this.getSafePage(page) - this.setState({page}) + this.setState({ page }) if (this.props.page !== page) { this.props.onPageChange(page) } @@ -60,7 +59,7 @@ export default class ReactTablePagination extends Component { onPageSizeChange, className, PreviousComponent = defaultButton, - NextComponent = defaultButton + NextComponent = defaultButton, } = this.props return ( @@ -70,7 +69,7 @@ export default class ReactTablePagination extends Component { >
{ + onClick={e => { if (!canPrevious) return this.changePage(page - 1) }} @@ -81,53 +80,51 @@ export default class ReactTablePagination extends Component {
- {this.props.pageText} {showPageJump ? ( -
- { - const val = e.target.value - const page = val - 1 - if (val === '') { - return this.setState({page: val}) - } - this.setState({page: this.getSafePage(page)}) - }} - value={this.state.page === '' ? '' : this.state.page + 1} - onBlur={this.applyPage} - onKeyPress={e => { - if (e.which === 13 || e.keyCode === 13) { - this.applyPage() - } - }} - /> -
- ) : ( - {page + 1} - )} {this.props.ofText} {pages || 1} + {this.props.pageText}{' '} + {showPageJump + ?
+ { + const val = e.target.value + const page = val - 1 + if (val === '') { + return this.setState({ page: val }) + } + this.setState({ page: this.getSafePage(page) }) + }} + value={this.state.page === '' ? '' : this.state.page + 1} + onBlur={this.applyPage} + onKeyPress={e => { + if (e.which === 13 || e.keyCode === 13) { + this.applyPage() + } + }} + /> +
+ : {page + 1}}{' '} + {this.props.ofText}{' '} + {pages || 1}
- {showPageSizeOptions && ( + {showPageSizeOptions && - - )} + }
{ + onClick={e => { if (!canNext) return this.changePage(page + 1) }} diff --git a/src/utils.js b/src/utils.js index 460eb02..1bc537c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -18,7 +18,7 @@ export default { splitProps, compactObject, isSortingDesc, - normalizeComponent + normalizeComponent, } function get (obj, path, def) { @@ -94,12 +94,14 @@ function remove (a, b) { function clone (a) { try { - return JSON.parse(JSON.stringify(a, (key, value) => { - if (typeof value === 'function') { - return value.toString() - } - return value - })) + return JSON.parse( + JSON.stringify(a, (key, value) => { + if (typeof value === 'function') { + return value.toString() + } + return value + }) + ) } catch (e) { return a } @@ -120,14 +122,10 @@ function sum (arr) { } function makeTemplateComponent (compClass) { - return ({children, className, ...rest}) => ( -
+ return ({ children, className, ...rest }) => +
{children}
- ) } function groupBy (xs, key) { @@ -149,10 +147,10 @@ function isArray (a) { function makePathArray (obj) { return flattenDeep(obj) - .join('.') - .replace('[', '.') - .replace(']', '') - .split('.') + .join('.') + .replace('[', '.') + .replace(']', '') + .split('.') } function flattenDeep (arr, newArr = []) { @@ -166,18 +164,22 @@ function flattenDeep (arr, newArr = []) { return newArr } -function splitProps ({className, style, ...rest}) { +function splitProps ({ className, style, ...rest }) { return { className, style, - rest + rest, } } function compactObject (obj) { const newObj = {} for (var key in obj) { - if (obj.hasOwnProperty(key) && obj[key] !== undefined && typeof obj[key] !== 'undefined') { + if ( + obj.hasOwnProperty(key) && + obj[key] !== undefined && + typeof obj[key] !== 'undefined' + ) { newObj[key] = obj[key] } } @@ -189,11 +191,9 @@ function isSortingDesc (d) { } function normalizeComponent (Comp, params = {}, fallback = Comp) { - return typeof Comp === 'function' ? ( - Object.getPrototypeOf(Comp).isReactComponent ? ( - - ) : Comp(params) - ) : fallback + return typeof Comp === 'function' + ? Object.getPrototypeOf(Comp).isReactComponent + ? + : Comp(params) + : fallback }