diff --git a/.storybook/config.js b/.storybook/config.js index ccdf258..63be681 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -12,13 +12,15 @@ import Readme from '../README.md' // import Simple from '../stories/Simple.js' import CellRenderers from '../stories/CellRenderers.js' -import MaxWidths from '../stories/MaxWidths.js' +import DefaultSorting from '../stories/DefaultSorting.js' +import CustomWidths from '../stories/CustomWidths.js' import ServerSide from '../stories/ServerSide.js' import SubComponents from '../stories/SubComponents.js' import Pivoting from '../stories/Pivoting.js' import PivotingSubComponents from '../stories/PivotingSubComponents.js' import OneHundredKRows from '../stories/OneHundredKRows.js' import FunctionalRendering from '../stories/FunctionalRendering.js' +import CustomExpanderPosition from '../stories/CustomExpanderPosition.js' // configure(() => { storiesOf('1. Docs') @@ -36,11 +38,13 @@ configure(() => { storiesOf('2. Demos') .add('Simple Table', Simple) .add('Cell Renderers & Custom Components', CellRenderers) - .add('Max Widths', MaxWidths) + .add('Default Sorting', DefaultSorting) + .add('Custom Column Widths', CustomWidths) .add('Server-side Data', ServerSide) .add('Sub Components', SubComponents) .add('Pivoting & Aggregation', Pivoting) .add('Pivoting & Aggregation w/ Sub Components', PivotingSubComponents) .add('100k Rows w/ Pivoting & Sub Components', OneHundredKRows) .add('Functional Rendering', FunctionalRendering) + .add('Custom Expander Position', CustomExpanderPosition) }, module) diff --git a/README.md b/README.md index 62ab7f8..5f12d6e 100644 --- a/README.md +++ b/README.md @@ -206,9 +206,14 @@ Or just define them on the component per-instance sortable: true, sort: 'asc' or 'desc', // used to determine the column sorting on init show: true, // can be used to hide a column + width: undefined, // A hardcoded width for the column. This overrides both min and max width options minWidth: 100 // A minimum width for this column. If there is extra room, column will flex to fill available space (up to the max-width, if set) 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 + // Cell Options className: '', // Set the classname of the `td` element of the column style: {}, // Set the style of the `td` element of the column diff --git a/react-table.css.zip b/react-table.css.zip deleted file mode 100644 index 5fc4196..0000000 Binary files a/react-table.css.zip and /dev/null differ diff --git a/react-table.js.zip b/react-table.js.zip deleted file mode 100644 index cda8814..0000000 Binary files a/react-table.js.zip and /dev/null differ diff --git a/src/index.js b/src/index.js index 52b5bd9..e131e97 100644 --- a/src/index.js +++ b/src/index.js @@ -235,12 +235,10 @@ export default React.createClass({ loading, pageSize, page, - sorting, + resolvedSorting, pages, // Pivoting State - pivotBy, pivotValKey, - pivotIDKey, subRowsKey, expandedRows, onExpandRow, @@ -256,19 +254,18 @@ export default React.createClass({ PaginationComponent, LoadingComponent, SubComponent, - columnPercentage, // Data model - pivotColumn, resolvedData, allVisibleColumns, headerGroups, - standardColumns, - allDecoratedColumns, hasHeaderGroups, // Sorted Data sortedData } = resolvedProps + // Determine the flex percentage for each column + // const columnPercentage = 100 / allVisibleColumns.length + // Pagination const startRow = pageSize * page const endRow = startRow + pageSize @@ -295,128 +292,145 @@ export default React.createClass({ const canPrevious = page > 0 const canNext = page + 1 < pages - const rowWidth = (SubComponent ? expanderColumnWidth : 0) + _.sum(allVisibleColumns.map(d => d.minWidth)) + const rowMinWidth = _.sum(allVisibleColumns.map(d => _.getFirstDefined(d.width, d.minWidth))) let rowIndex = -1 - const makeHeaderGroup = () => ( + const makeHeaderGroups = () => ( - {pivotBy.length ? ( - - ) : SubComponent ? ( - - ) : null} - {headerGroups.map((column, i) => { - return ( - { - return d.maxWidth < d.minWidth ? d.maxWidth : d.minWidth - }))}px`, - maxWidth: `${_.sum(column.columns.map(d => d.maxWidth))}px` - }))} - > - {typeof column.header === 'function' ? ( - - ) : column.header} - - ) - })} + {headerGroups.map(makeHeaderGroup)} ) - const makeHeader = () => { - const pivotSort = pivotColumn && sorting.find(d => d.id === pivotColumn.id) + const makeHeaderGroup = (column, i) => { + const flex = _.sum(column.columns.map(d => d.width ? 0 : d.minWidth)) + const width = _.sum(column.columns.map(d => _.getFirstDefined(d.width, d.minWidth))) + const maxWidth = _.sum(column.columns.map(d => _.getFirstDefined(d.width, d.maxWidth))) + if (column.expander) { + if (column.pivotColumns) { + return ( + + ) + } + return ( + + ) + } + return ( + + {typeof column.header === 'function' ? ( + + ) : column.header} + + ) + } + + const makeHeaders = () => { return ( - {pivotBy.length ? ( - { - pivotColumn.sortable && this.sortColumn(pivotColumn.pivotColumns, e.shiftKey) - }} - > - {pivotColumn.pivotColumns.map((column, i) => { - return ( - - {typeof column.header === 'function' ? ( - - ) : column.header} - {i < pivotColumn.pivotColumns.length - 1 && ( - - )} - - ) - })} - - ) : SubComponent ? ( - - ) : null} - {standardColumns.map(makeHeaderGroupColumn)} + {allVisibleColumns.map(makeHeader)} ) } - const makeHeaderGroupColumn = (column, i) => { - const sort = sorting.find(d => d.id === column.id) + + const makeHeader = (column, i) => { + const sort = resolvedSorting.find(d => d.id === column.id) const show = typeof column.show === 'function' ? column.show() : column.show + const width = _.getFirstDefined(column.width, column.minWidth) + const maxWidth = _.getFirstDefined(column.width, column.maxWidth) + if (column.expander) { + if (column.pivotColumns) { + const pivotSort = resolvedSorting.find(d => d.id === column.id) + return ( + { + column.sortable && this.sortColumn(column.pivotColumns, e.shiftKey) + }} + > + {column.pivotColumns.map((pivotColumn, i) => { + return ( + + {typeof pivotColumn.header === 'function' ? ( + + ) : pivotColumn.header} + {i < column.pivotColumns.length - 1 && ( + + )} + + ) + })} + + ) + } + return ( + + ) + } + return ( { column.sortable && this.sortColumn(column, e.shiftKey) @@ -460,8 +474,6 @@ export default React.createClass({ subRows: row[subRowsKey] } const isExpanded = _.get(expandedRows, rowInfo.nestingPath) - const rowPivotColumn = allDecoratedColumns.find(d => d.id === row[pivotIDKey]) - const PivotCell = rowPivotColumn && rowPivotColumn.pivotRender return ( - {(pivotBy.length || SubComponent) && ( - { + {allVisibleColumns.map((column, i2) => { + const Cell = column.render + const show = typeof column.show === 'function' ? column.show() : column.show + const width = _.getFirstDefined(column.width, column.minWidth) + const maxWidth = _.getFirstDefined(column.width, column.maxWidth) + + if (column.expander) { + const onTdClick = (e) => { if (onExpandRow) { return onExpandRow(rowInfo.nestingPath, e) } @@ -491,40 +501,73 @@ export default React.createClass({ return this.setStateWithData({ expandedRows: _.set(newExpandedRows, rowInfo.nestingPath, {}) }) - }} - > - {rowInfo.subRows ? ( - - - {rowPivotColumn && rowPivotColumn.pivotRender ? ( - + {rowInfo.subRows ? ( + + + {column && column.pivotRender ? ( + + ) : {row[pivotValKey]} ({rowInfo.subRows.length})} + + ) : SubComponent ? ( + + + + ) : null} + + ) + } + + // Return the regular expander cell + return ( + + + - ) : {row[pivotValKey]} ({rowInfo.subRows.length})} - - ) : SubComponent ? ( - - - - ) : null} - - )} - {standardColumns.map((column, i2) => { - const Cell = column.render - const show = typeof column.show === 'function' ? column.show() : column.show + + + ) + } + + // Return regular cell return ( {typeof Cell === 'function' ? ( @@ -566,15 +609,18 @@ export default React.createClass({ })} /> )} - {standardColumns.map((column, i2) => { + {allVisibleColumns.map((column, i2) => { const show = typeof column.show === 'function' ? column.show() : column.show + const width = _.getFirstDefined(column.width, column.minWidth) + const maxWidth = _.getFirstDefined(column.width, column.maxWidth) return (   @@ -595,12 +641,12 @@ export default React.createClass({ className={classnames(tableClassName)} style={tableStyle} > - {hasHeaderGroups && makeHeaderGroup()} - {makeHeader()} + {hasHeaderGroups && makeHeaderGroups()} + {makeHeaders()} {pageRows.map((d, i) => makePageRow(d, i))} @@ -635,7 +681,7 @@ export default React.createClass({ padRows, canPrevious, canNext, - rowWidth + rowMinWidth } return children ? children(childState, makeTable, this) : makeTable() @@ -649,7 +695,9 @@ export default React.createClass({ data, pivotIDKey, pivotValKey, - subRowsKey + subRowsKey, + expanderColumnWidth, + SubComponent } = this.getResolvedState(nextProps, nextState) // Determine Header Groups @@ -665,21 +713,67 @@ export default React.createClass({ let currentSpan = [] // A convenience function to add a header and reset the currentSpan - const addHeader = (columns, column = {}) => { + const addHeader = (columns, column = columns[0]) => { headerGroups.push(Object.assign({}, column, { columns: columns })) currentSpan = [] } + const noSubExpanderColumns = columns.map(col => { + return { + ...col, + columns: col.columns ? col.columns.filter(d => !d.expander) : undefined + } + }) + + 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 + } + + const makeDecoratedColumn = (column) => { + const dcol = Object.assign({}, this.props.column, column) + + if (dcol.expander) { + dcol.width = expanderColumnWidth + return dcol + } + + 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 + } + // Decorate the columns const decorateAndAddToAll = (col) => { - const decoratedColumn = this.makeDecoratedColumn(col) + const decoratedColumn = makeDecoratedColumn(col) allDecoratedColumns.push(decoratedColumn) return decoratedColumn } let allDecoratedColumns = [] - const decoratedColumns = columns.map((column, i) => { + const decoratedColumns = columnsWithExpander.map((column, i) => { if (column.columns) { return { ...column, @@ -690,7 +784,7 @@ export default React.createClass({ } }) - // Build the visible columns and headers and flat column list + // Build the visible columns, headers and flat column list let visibleColumns = decoratedColumns.slice() let allVisibleColumns = [] @@ -709,7 +803,23 @@ export default React.createClass({ return column.columns ? column.columns.length : pivotBy.indexOf(column.id) > -1 ? false : _.getFirstDefined(column.show, true) }) - // Build allVisible columns and HeaderGroups + // Move the pivot columns into a single column if needed + if (pivotBy.length) { + const pivotColumns = [] + for (var i = 0; i < allDecoratedColumns.length; i++) { + if (pivotBy.indexOf(allDecoratedColumns[i].id) > -1) { + pivotColumns.push(allDecoratedColumns[i]) + } + } + const pivotColumn = { + ...pivotColumns[0], + pivotColumns, + expander: true + } + visibleColumns[expanderColumnIndex] = pivotColumn + } + + // Build flast list of allVisibleColumns and HeaderGroups visibleColumns.forEach((column, i) => { if (column.columns) { allVisibleColumns = allVisibleColumns.concat(column.columns) @@ -726,23 +836,6 @@ export default React.createClass({ addHeader(currentSpan) } - // Move the pivot columns into a single column if needed - if (pivotBy.length) { - const pivotColumns = [] - for (var i = 0; i < allDecoratedColumns.length; i++) { - if (pivotBy.indexOf(allDecoratedColumns[i].id) > -1) { - pivotColumns.push(allDecoratedColumns[i]) - } - } - allVisibleColumns.unshift({ - ...pivotColumns[0], - pivotColumns - }) - } - - // Determine the flex percentage for each column - const columnPercentage = 100 / allVisibleColumns.length - // Access the data let resolvedData = data.map((d, i) => { const row = { @@ -750,6 +843,7 @@ export default React.createClass({ __index: i } allDecoratedColumns.forEach(column => { + if (column.expander) return row[column.id] = column.accessor(d) }) return row @@ -802,11 +896,9 @@ export default React.createClass({ return { resolvedData, - columnPercentage, pivotColumn, allVisibleColumns, headerGroups, - standardColumns, allDecoratedColumns, hasHeaderGroups } @@ -824,6 +916,7 @@ export default React.createClass({ // Resolve the data from either manual data or sorted data return { + resolvedSorting, sortedData: manual ? resolvedData : this.sortData(resolvedData, resolvedSorting) } }, @@ -850,10 +943,12 @@ export default React.createClass({ } }) - return initSorting.length ? initSorting : [{ - id: columns[0].id, - asc: true - }] + return initSorting + + // return initSorting.length ? initSorting : [{ + // id: columns.find(d => d.id).id, + // asc: true + // }] }, sortData (data, sorting) { const sorted = _.sortBy(data, sorting.map(sort => { @@ -875,32 +970,7 @@ export default React.createClass({ } }) }, - makeDecoratedColumn (column) { - const dcol = Object.assign({}, 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 - }, getMinRows () { return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize')) }, diff --git a/stories/CustomExpanderPosition.js b/stories/CustomExpanderPosition.js new file mode 100644 index 0000000..a8cf553 --- /dev/null +++ b/stories/CustomExpanderPosition.js @@ -0,0 +1,108 @@ +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(5553), d => { + return { + firstName: namor.generate({ words: 1, numLen: 0 }), + lastName: namor.generate({ words: 1, numLen: 0 }), + age: Math.floor(Math.random() * 30) + } + }) + + const columns = [{ + header: 'Name', + columns: [{ + header: 'First Name', + accessor: 'firstName', + render: row => { + return {row.aggregated ? '...' : row.value} + } + }, { + header: 'Last Name', + id: 'lastName', + accessor: d => d.lastName + }] + }, { + expander: true + }, { + header: 'Info', + columns: [{ + header: 'Age', + accessor: 'age', + aggregate: vals => _.round(_.mean(vals)), + render: row => { + return {row.aggregated ? `${row.value} (avg)` : row.value} + } + }] + }] + + return ( +
+
+ Hello} + pivotBy={['lastName']} + /> +
+
+
+ Tip: Hold shift when sorting to multi-sort! +
+ {() => getCode()} +
+ ) +} + +function getCode () { + return ` +import ReactTable from 'react-table' + +// Create some column definitions +const columns = [{ + header: 'Name', + columns: [{ + header: 'First Name', + accessor: 'firstName', + render: row => { + return {row.aggregated ? '...' : row.value} + } + }, { + header: 'Last Name', + id: 'lastName', + accessor: d => d.lastName + }] +}, { + expander: true +}, { + header: 'Info', + columns: [{ + header: 'Age', + accessor: 'age', + aggregate: vals => _.round(_.mean(vals)), + render: row => { + return {row.aggregated ? \`$\{row.value} (avg)\` : row.value} + } + }] +}] + +return ( + Hello} + pivotBy={['lastName']} + /> +) + ` +} diff --git a/stories/MaxWidths.js b/stories/CustomWidths.js similarity index 95% rename from stories/MaxWidths.js rename to stories/CustomWidths.js index 982ec90..cb56552 100644 --- a/stories/MaxWidths.js +++ b/stories/CustomWidths.js @@ -24,14 +24,14 @@ export default () => { header: 'Last Name', id: 'lastName', accessor: d => d.lastName, - maxWidth: 400 + width: 300 }] }, { header: 'Info', columns: [{ header: 'Age', accessor: 'age', - maxWidth: 60 + minWidth: 400 }] }] @@ -68,14 +68,14 @@ const columns = [{ header: 'Last Name', id: 'lastName', accessor: d => d.lastName, - maxWidth: 400 + width: 300 }] }, { header: 'Info', columns: [{ header: 'Age', accessor: 'age', - maxWidth: 60 + minWidth: 400 }] }] diff --git a/stories/DefaultSorting.js b/stories/DefaultSorting.js new file mode 100644 index 0000000..8a1705c --- /dev/null +++ b/stories/DefaultSorting.js @@ -0,0 +1,87 @@ +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(5553), d => { + return { + firstName: namor.generate({ words: 1, numLen: 0 }), + lastName: namor.generate({ words: 1, numLen: 0 }), + age: Math.floor(Math.random() * 30) + } + }) + + 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', + sort: 'desc' + }] + }] + + return ( +
+
+ +
+
+
+ Tip: Hold shift when sorting to multi-sort! +
+ {() => getCode()} +
+ ) +} + +function getCode () { + return ` +import ReactTable from 'react-table' + +// Create some column definitions +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' + }] +}] + +// Display your table! +return ( + +) + ` +}