diff --git a/.storybook/config.js b/.storybook/config.js index 585424b..b064361 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -26,6 +26,7 @@ import NoDataText from '../stories/NoDataText.js' import Footers from '../stories/Footers.js' import Filtering from '../stories/Filtering.js' import ControlledTable from '../stories/ControlledTable.js' +import PivotingOptions from '../stories/PivotingOptions.js' // configure(() => { storiesOf('1. Docs') @@ -51,6 +52,7 @@ configure(() => { .add('Pivoting & Aggregation', Pivoting) .add('Pivoting & Aggregation w/ Sub Components', PivotingSubComponents) .add('100k Rows w/ Pivoting & Sub Components', OneHundredKRows) + .add('Pivoting Options', PivotingOptions) .add('Functional Rendering', FunctionalRendering) .add('Custom Expander Position', CustomExpanderPosition) .add('Custom "No Data" Text', NoDataText) diff --git a/README.md b/README.md index 93c03de..d78a86a 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,6 @@ These are all of the available props (and their default values) for the main ` ) }, + + // Global Expander Column Defaults + expanderDefaults: { + sortable: false, + width: 35, + hideFilter: true + // render: will be overriden in methods.js to display ExpanderComponent + }, + + // Global Pivot Column Defaults + pivotDefaults: { + // render: will be overriden in methods.js to display ExpanderComponent + }, // Text previousText: 'Previous', @@ -307,8 +319,13 @@ Or just define them as props maxWidth: undefined, // A maximum width for this column. // Special - expander: false, // This option will override all data-related options and designates the column to be used - // for pivoting and sub-component expansion + // Turns this column into a special column for specifying expander and pivot column options. + // If this option is true and there is NOT a pivot column, the `expanderDefaults` options will be applied on top of the column options. + // If this option is true and there IS a pivot column, the `pivotDefaults` options will be applied on top of the column options. + // Adding a column with the `expander` option set will allow you to rearrange expander and pivot column orderings in the table. + // It will also let you specify rendering of the header (and header group if this special column is placed in the `columns` option of another column) and + // the rendering of the expander itself. + expander: false, // Cell Options className: '', // Set the classname of the `td` element of the column diff --git a/src/defaultProps.js b/src/defaultProps.js index be40182..7cf413c 100644 --- a/src/defaultProps.js +++ b/src/defaultProps.js @@ -15,7 +15,6 @@ export default { pageSizeOptions: [5, 10, 20, 25, 50, 100], defaultPageSize: 20, showPageJump: true, - expanderColumnWidth: 35, collapseOnSortingChange: true, collapseOnPageChange: true, collapseOnDataChange: true, @@ -120,6 +119,19 @@ export default { ) }, + // Global Expander Column Defaults + expanderDefaults: { + sortable: false, + width: 35, + hideFilter: true + // render: will be overriden in methods.js to display ExpanderComponent + }, + + // Global Pivot Column Defaults + pivotDefaults: { + // render: will be overriden in methods.js to display ExpanderComponent + }, + // Text previousText: 'Previous', nextText: 'Next', @@ -150,12 +162,11 @@ export default { }, TdComponent: _.makeTemplateComponent('rt-td'), TfootComponent: _.makeTemplateComponent('rt-tfoot'), - ExpanderComponent: ({isExpanded, ...rest}) => { + ExpanderComponent: ({isExpanded}) => { return ( -
+
+ • +
) }, PaginationComponent: Pagination, diff --git a/src/index.js b/src/index.js index 5a6a3a6..6d19f76 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ export const ReactTableDefaults = defaultProps export default class ReactTable extends Methods(Lifecycle(Component)) { static defaultProps = defaultProps + constructor (props) { super() @@ -41,6 +42,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { skipNextSort: false } } + render () { const resolvedState = this.getResolvedState() const { @@ -70,7 +72,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { getNoDataProps, getResizerProps, showPagination, - expanderColumnWidth, manual, loadingText, noDataText, @@ -98,7 +99,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ThComponent, TdComponent, TfootComponent, - ExpanderComponent, PaginationComponent, LoadingComponent, SubComponent, @@ -229,39 +229,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { maxWidth: `${maxWidth}px` } - if (column.expander) { - if (column.pivotColumns) { - return ( - - ) - } - return ( - - ) - } return ( + _.normalizeComponent(column.render, { + data: sortedData, + column: column + }) )} ) @@ -382,21 +352,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) } - return ( - - ) } return ( @@ -500,7 +455,15 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) if (i < column.pivotColumns.length - 1) { - pivotCols.push() + pivotCols.push( + _.normalizeComponent(column.filterRender, + { + column, + filter, + key: col.id + '-' + i + }, + defaultProps.column.filterRender + )) } } return ( @@ -524,21 +487,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) } - return ( - - ) } const filter = filtering.find(filter => filter.id === column.id) @@ -618,6 +566,8 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ...columnProps.style } + const extraProps = {} + if (column.expander) { const onTdClick = (e) => { if (onExpandRow) { @@ -634,6 +584,8 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { }) } + extraProps['onClick'] = onTdClick + if (column.pivotColumns) { // Return the pivot expander cell const PivotCell = column.pivotRender @@ -656,9 +608,11 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { > {rowInfo.subRows ? ( - + {_.normalizeComponent(column.render, { + ...rowInfo, + value: rowInfo.rowValues[column.id], + isExpanded + }, rowInfo.rowValues[column.id])} {column && column.pivotRender ? ( ) : SubComponent ? ( - + {_.normalizeComponent(column.render, { + ...rowInfo, + value: rowInfo.rowValues[column.id], + isExpanded + }, rowInfo.rowValues[column.id])} ) : null} ) } - - // Return the regular expander cell - return ( - - - - - - ) } // Return regular cell @@ -716,10 +649,12 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { maxWidth: `${maxWidth}px` }} {...tdProps.rest} + {...extraProps} > {_.normalizeComponent(column.render, { ...rowInfo, - value: rowInfo.rowValues[column.id] + value: rowInfo.rowValues[column.id], + isExpanded }, rowInfo.rowValues[column.id])} ) @@ -859,22 +794,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) } - - // Return the regular expander cell - return ( - - ) } // Return regular cell diff --git a/src/methods.js b/src/methods.js index 7ed5831..96f7bb8 100644 --- a/src/methods.js +++ b/src/methods.js @@ -10,6 +10,7 @@ export default Base => class extends Base { } return resolvedState } + getDataModel (newState) { const { columns, @@ -18,7 +19,6 @@ export default Base => class extends Base { pivotIDKey, pivotValKey, subRowsKey, - expanderColumnWidth, SubComponent } = newState @@ -44,29 +44,42 @@ export default Base => class extends Base { currentSpan = [] } - const noSubExpanderColumns = columns.map(col => { - return { - ...col, - columns: col.columns ? col.columns.filter(d => !d.expander) : undefined - } - }) + let columnsWithExpander = [...columns] - let expanderColumnIndex = columns.findIndex(col => col.expander) - const needsExpander = (SubComponent || pivotBy.length) && expanderColumnIndex === -1 - const columnsWithExpander = needsExpander ? [{expander: true}, ...noSubExpanderColumns] : noSubExpanderColumns - if (needsExpander) { - expanderColumnIndex = 0 + 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 it has subrows or pivot columns we need to make sure we have an expander column + if ((SubComponent || pivotBy.length) && !expanderColumn) { + expanderColumn = {expander: true} + columnsWithExpander = [expanderColumn, ...columnsWithExpander] } const makeDecoratedColumn = (column) => { - const dcol = { - ...this.props.column, - ...column - } - - if (dcol.expander) { - dcol.width = expanderColumnWidth - return dcol + let dcol + if (pivotBy.length && column.expander) { + dcol = { + ...this.props.column, + render: this.props.ExpanderComponent, + filterRender: this.props.ExpanderComponent, + ...this.props.pivotDefaults, + ...column + } + } else if (column.expander) { + dcol = { + ...this.props.column, + render: this.props.ExpanderComponent, + ...this.props.expanderDefaults, + ...column + } + } else { + dcol = { + ...this.props.column, + ...column + } } if (typeof dcol.accessor === 'string') { @@ -138,12 +151,24 @@ export default Base => class extends Base { pivotColumns.push(allDecoratedColumns[i]) } } - const pivotColumn = { - ...pivotColumns[0], - pivotColumns, - expander: true + + const pivotExpanderColumn = visibleColumns.findIndex(col => col.expander || (col.columns && col.columns.some(col2 => col2.expander))) + if (pivotExpanderColumn >= 0) { + const pivotColumn = { + ...visibleColumns[pivotExpanderColumn], + pivotColumns + } + visibleColumns[pivotExpanderColumn] = pivotColumn + } else { + // If the expander column wasn't on the top level column, find it in the `columns` option. + const pivotExpanderSubColumn = visibleColumns[pivotExpanderColumn].columns.findIndex(col => col.expander) + const pivotColumn = { + ...visibleColumns[pivotExpanderColumn].columns[pivotExpanderSubColumn], + pivotColumns + } + // Add the pivot columns to the expander column + visibleColumns[pivotExpanderColumn].columns[pivotExpanderSubColumn] = pivotColumn } - visibleColumns[expanderColumnIndex] = pivotColumn } // Build flast list of allVisibleColumns and HeaderGroups @@ -185,8 +210,7 @@ export default Base => class extends Base { }) return aggregationValues } - let standardColumns = pivotBy.length ? allVisibleColumns.slice(1) : allVisibleColumns - const aggregatingColumns = standardColumns.filter(d => d.aggregate) + const aggregatingColumns = allVisibleColumns.filter(d => !d.expander && d.aggregate) let pivotColumn if (pivotBy.length) { pivotColumn = allVisibleColumns[0] diff --git a/stories/CustomExpanderPosition.js b/stories/CustomExpanderPosition.js index 9648c72..3277b09 100644 --- a/stories/CustomExpanderPosition.js +++ b/stories/CustomExpanderPosition.js @@ -8,8 +8,8 @@ import ReactTable from '../src/index' export default () => { const data = _.map(_.range(5553), d => { return { - firstName: namor.generate({ words: 1, numLen: 0 }), - lastName: namor.generate({ words: 1, numLen: 0 }), + firstName: namor.generate({words: 1, numLen: 0}), + lastName: namor.generate({words: 1, numLen: 0}), age: Math.floor(Math.random() * 30) } }) @@ -19,26 +19,33 @@ export default () => { columns: [{ header: 'First Name', accessor: 'firstName', - render: row => { - return {row.aggregated ? '...' : row.value} - } + footer: () =>
First Name
}, { header: 'Last Name', - id: 'lastName', - accessor: d => d.lastName + accessor: 'lastName', + footer: () =>
Last Name
}] }, { header: 'Info', columns: [{ header: 'Age', accessor: 'age', - aggregate: vals => _.round(_.mean(vals)), - render: row => { - return {row.aggregated ? `${row.value} (avg)` : row.value} - } + footer: () =>
Age
}] }, { - expander: true + header: 'Expand', + columns: [{ + expander: true, + header: () => (More), + width: 65, + render: ({isExpanded, ...rest}) => ( +
+ {isExpanded ? : } +
+ ), + style: {cursor: 'pointer', fontSize: 25, padding: '0', textAlign: 'center', userSelect: 'none'}, + footer: () => + }] }] return ( @@ -71,26 +78,33 @@ const columns = [{ columns: [{ header: 'First Name', accessor: 'firstName', - render: row => { - return {row.aggregated ? '...' : row.value} - } + footer: () =>
First Name
}, { header: 'Last Name', - id: 'lastName', - accessor: d => d.lastName + accessor: 'lastName', + footer: () =>
Last Name
}] }, { header: 'Info', columns: [{ header: 'Age', accessor: 'age', - aggregate: vals => _.round(_.mean(vals)), - render: row => { - return {row.aggregated ? \`$\{row.value} (avg)\` : row.value} - } + footer: () =>
Age
}] }, { - expander: true + header: 'Expand', + columns: [{ + expander: true, + header: () => (More), + width: 65, + render: ({isExpanded, ...rest}) => ( +
+ {isExpanded ? : } +
+ ), + style: {cursor: 'pointer', fontSize: 25, padding: '0', textAlign: 'center', userSelect: 'none'}, + footer: () => + }] }] return ( diff --git a/stories/PivotingOptions.js b/stories/PivotingOptions.js new file mode 100644 index 0000000..97ebb36 --- /dev/null +++ b/stories/PivotingOptions.js @@ -0,0 +1,183 @@ +import React from 'react' +import _ from 'lodash' +import namor from 'namor' + +import CodeHighlight from './components/codeHighlight' +import ReactTable from '../src/index' + +export default () => { + const data = _.map(_.range(1000), d => { + return { + firstName: namor.generate({words: 1, numLen: 0}), + lastName: namor.generate({words: 1, numLen: 0}), + age: Math.floor(Math.random() * 30), + visits: Math.floor(Math.random() * 100) + } + }) + + 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', + aggregate: vals => { + return _.round(_.mean(vals)) + }, + render: row => { + return {row.aggregated ? `${row.value} (avg)` : row.value} + } + }, { + header: 'Visits', + accessor: 'visits', + aggregate: vals => _.sum(vals), + hideFilter: true + }] + }, { + header: () => First ⇢ Last Name Pivot, + expander: true, + minWidth: 200, + render: ({isExpanded, ...rest}) => ( + isExpanded ? : + ), + filterRender: ({key}) => , + footer: () =>
Pivot Footer
+ }] + + return ( +
+
+ { + return ( +
+ You can put any component you want here, even another React Table! +
+
+ !x.expander)} + defaultPageSize={3} + showPagination={false} + SubComponent={(row) => { + return ( +
+ It even has access to the row data: + {() => JSON.stringify(row, null, 2)} +
+ ) + }} + /> +
+ ) + }} + /> +
+
+
+ Tip: Hold shift when sorting to multi-sort! +
+ {() => getCode()} +
+ ) +} + +function getCode () { + return ` +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', + aggregate: vals => { + return _.round(_.mean(vals)) + }, + render: row => { + return {row.aggregated ? \`\${row.value} (avg)\` : row.value} + } + }, { + header: 'Visits', + accessor: 'visits', + aggregate: vals => _.sum(vals), + hideFilter: true + }] +}, { + header: () => First ⇢ Last Name Pivot, + expander: true, + minWidth: 200, + render: ({isExpanded, ...rest}) => ( + isExpanded ? : + ), + filterRender: ({key}) => , + footer: () =>
Pivot Footer
+}] + +return ( +
+
+ { + return ( +
+ You can put any component you want here, even another React Table! +
+
+ !x.expander)} + defaultPageSize={3} + showPagination={false} + SubComponent={(row) => { + return ( +
+ It even has access to the row data: + {() => JSON.stringify(row, null, 2)} +
+ ) + }} + /> +
+ ) + }} + /> +
+
+
+ Tip: Hold shift when sorting to multi-sort! +
+ {() => getCode()} +
+) + ` +}