diff --git a/README.md b/README.md index 5f12d6e..227bfcf 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,10 @@ - [Data](#data) - [Props](#props) - [Columns](#columns) +- [Column Header Groups](#column-header-groups) +- [Custom Cell & Header Rendering](#custom-cell--and-header-rendering) - [Styles](#styles) -- [Header Groups](#header-groups) +- [Custom Props](#custom-props) - [Pivoting & Aggregation](#pivoting--aggregation) - [Sub Tables & Sub Components](#sub-tables--sub-components) - [Server-side Data](#server-side-data) @@ -52,6 +54,10 @@ - [Functional Rendering](#functional-rendering) - [Multi-Sort](#multi-sort) - [Component Overrides](#component-overrides) +- [Contributing](#contributing) +- [Scripts](#scripts) +- [Used By](#used-by) + ## Installation 1. Install React Table as a dependency @@ -121,55 +127,87 @@ These are all of the available props (and their default values) for the main ` null, // Anytime the internal state of the table changes, this will fire - onTrClick: (row, event) => null, // Handler for row click events + // Controlled State Overrides (see Fully Controlled Component section) + page: undefined, + pageSize: undefined, + sorting: undefined + + // Controlled State Callbacks + onExpandSubComponent: undefined, + onPageChange: undefined, + onPageSizeChange: undefined, + onSortingChange: undefined, + + // Pivoting + pivotBy: undefined, + pivotColumnWidth: 200, + pivotValKey: '_pivotVal', + pivotIDKey: '_pivotID', + subRowsKey: '_subRows', + + // Pivoting State Overrides (see Fully Controlled Component section) + expandedRows: {}, + + // Pivoting State Callbacks + onExpandRow: undefined, + + // General Callbacks + onChange: () => null, + + // Classes + className: '', + style: {}, + + // Component decorators + getProps: () => ({}), + getTableProps: () => ({}), + getTheadGroupProps: () => ({}), + getTheadGroupTrProps: () => ({}), + getTheadGroupThProps: () => ({}), + getTheadProps: () => ({}), + getTheadTrProps: () => ({}), + getTheadThProps: () => ({}), + getTbodyProps: () => ({}), + getTrGroupProps: () => ({}), + getTrProps: () => ({}), + getThProps: () => ({}), + getTdProps: () => ({}), + getPaginationProps: () => ({}), + getLoadingProps: () => ({}), + + // Global Column Defaults + column: { + sortable: true, + show: true, + minWidth: 100, + // Cells only + render: undefined, + className: '', + style: {}, + getProps: () => ({}), + // Headers only + header: undefined, + headerClassName: '', + headerStyle: {}, + getHeaderProps: () => ({}) + }, // Text previousText: 'Previous', nextText: 'Next', + loadingText: 'Loading...', pageText: 'Page', ofText: 'of', rowsText: 'rows', - - // Classes - className: '-striped -highlight', // The most top level className for the component - tableClassName: '', // ClassName for the `table` element - theadClassName: '', // ClassName for the `thead` element - tbodyClassName: '', // ClassName for the `tbody` element - trClassName: '', // ClassName for all `tr` elements - trClassCallback: row => null, // A call back to dynamically add classes (via the classnames module) to a row element - paginationClassName: '' // ClassName for `pagination` element - - // Styles - style: {}, // Main style object for the component - tableStyle: {}, // style object for the `table` component - theadStyle: {}, // style object for the `thead` component - tbodyStyle: {}, // style object for the `tbody` component - trStyle: {}, // style object for the `tr` component - trStyleCallback: row => {}, // A call back to dynamically add styles to a row element - thStyle: {}, // style object for the `th` component - tdStyle: {}, // style object for the `td` component - paginationStyle: {}, // style object for the `paginination` component - - // Controlled Props (see Using as a Fully Controlled Component below) - page: undefined, - pageSize: undefined, - sorting: undefined, - expandedRows: undefined, - // Controlled Callbacks - onExpandRow: undefined, - onPageChange: undefined, - onPageSizeChange: undefined, } ``` @@ -185,7 +223,7 @@ Object.assign(ReactTableDefaults, { }) ``` -Or just define them on the component per-instance +Or just define them as props ```javascript {value}, // Provide a JSX element or stateless function to render whatever you want as the column's cell with access to the entire row + render: JSX eg. (rowInfo: {value, rowValues, row, index, viewIndex}) => {value}, // Provide a JSX element or stateless function to render whatever you want as the column's cell with access to the entire row // value == the accessed value of the column // rowValues == an object of all of the accessed values for the row // row == the original row of data supplied to the table @@ -235,19 +273,12 @@ Or just define them on the component per-instance }] ``` -## Styles -React-table ships with a minimal and clean stylesheet to get you on your feet quickly. It's located at `react-table/react-table.css`. - -- Adding a `-striped` className to ReactTable will slightly color odd numbered rows for legibility -- Adding a `-highlight` className to ReactTable will highlight any row as you hover over it - -We think the default styles looks great! But, if you prefer a more custom look, all of the included styles are easily overridable. Every single component contains a unique class that makes it super easy to customize. Just go for it! - -## Header Groups -To group columns with another header column, just nest your columns in a header column like so: +## Column Header Groups +To group columns with another header column, just nest your columns in a header column. Header columns utilize the same header properties as regular columns. ```javascript const columns = [{ header: 'Favorites', + headerClassName: 'my-favorites-column-header-group' columns: [{ header: 'Color', accessor: 'favorites.color' @@ -261,6 +292,146 @@ const columns = [{ }] ``` +## Custom Cell & Header Rendering +You can use any react component or JSX to display column headers or cells. Any component you use will be passed the following props: +- `row` - Original row from your data +- `rowValues` - The post-accessed values from the original row +- `index` - The index of the row +- `viewIndex` - the index of the row relative to the current page +- `level` - The nesting depth (zero-indexed) +- `nestingPath` - The nesting path of the row +- `aggregated` - A boolean stating if the row is an aggregation row +- `subRows` - An array of any expandable sub-rows contained in this row + +```javascript +// This column uses a stateless component to produce a different colored bar depending on the value +// You can also use stateful components or any other function that returns JSX +const columns = [{ + header: () => Progress, + accessor: 'progress', + render: row => ( +
+
66 ? '#85cc00' + : row.value > 33 ? '#ffbf00' + : '#ff2e00', + borderRadius: '2px', + transition: 'all .2s ease-out' + }} + /> +
+ ) +}] +``` + +## Styles +React-table ships with a minimal and clean stylesheet to get you on your feet quickly. It's located at `react-table/react-table.css`. + +#### Built-in Styles +- Adding a `-striped` className to ReactTable will slightly color odd numbered rows for legibility +- Adding a `-highlight` className to ReactTable will highlight any row as you hover over it + +#### CSS Styles +We think the default styles looks great! But, if you prefer a more custom look, all of the included styles are easily overridable. Every single component contains a unique class that makes it super easy to customize. Just go for it! + +#### JS Styles +Every single react-table element and `get[ComponentName]Props` callback support classes (powered by `classname` and js styles. + +## Custom Props + +#### Built-in Components +Every single built-in component's props can be dynamically extended using any one of these prop-callbacks: +```javascript + +``` + +These callbacks are executed with each render of the element with three parameters: +1. Table State +1. RowInfo (where applicable) +1. Column (where applicable) + +This makes it extremely easy to add, say... a row click callback! +```javascript +// When any Td element is clicked, we'll log out some information + { + return { + onClick: e => { + console.log('A Td Element was clicked!') + console.log('It was in this column:', column) + console.log('It was in this row:', rowInfo) + console.log('it produced this event:', e) + } + } + }} +/> +``` + +You can use these callbacks for dynamic styling as well! +```javascript +// Any Tr element will be green if its (row.age > 20) + { + return { + style: { + background: rowInfo.age > 20 ? 'green' : 'red' + } + } + }} +/> +``` + +#### Column Components +Just as core components can have dynamic props, columns and column headers can too! + +You can utilize either of these prop callbacks on columns: +```javascript +const columns = [{ + getHeaderProps: () => (...), + getProps: () => (...) +}] +``` + +In a similar fashion these can be used to dynamically style just about anything! +```javascript +// This columns cells will be red if (row.name === Santa Clause) +const columns = [{ + getProps: (state, rowInfo, column) => { + return { + style: { + background: rowInfo.name === 'Santa Clause' ? 'red' : null + } + } + } +}] +``` + ## Pivoting & Aggregation Pivoting the table will group records together based on their accessed values and allow the rows in that group to be expanded underneath it. To pivot, pass an array of `columnID`'s to `pivotBy`. Remember, a column's `id` is either the one that you assign it (when using a custom accessors) or its `accessor` string. diff --git a/src/componentMethods.js b/src/componentMethods.js new file mode 100644 index 0000000..f7d86cf --- /dev/null +++ b/src/componentMethods.js @@ -0,0 +1,400 @@ +import _ from './utils' + +export default { + getDataModel (nextProps, nextState) { + const { + columns, + pivotBy = [], + data, + pivotIDKey, + pivotValKey, + subRowsKey, + expanderColumnWidth, + SubComponent + } = this.getResolvedState(nextProps, nextState) + + // Determine Header Groups + let hasHeaderGroups = false + columns.forEach(column => { + if (column.columns) { + hasHeaderGroups = true + } + }) + + // Build Header Groups + const headerGroups = [] + let currentSpan = [] + + // A convenience function to add a header and reset the currentSpan + 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 = 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) + }) + + // 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) + 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 + let resolvedData = data.map((d, i) => { + const row = { + __original: d, + __index: i + } + allDecoratedColumns.forEach(column => { + if (column.expander) return + row[column.id] = column.accessor(d) + }) + return row + }) + + // 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 + } + let standardColumns = pivotBy.length ? allVisibleColumns.slice(1) : allVisibleColumns + const aggregatingColumns = standardColumns.filter(d => d.aggregate) + let pivotColumn + if (pivotBy.length) { + pivotColumn = allVisibleColumns[0] + 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 + } + } + ) + // Recurse into the subRows + groupedRows = groupedRows.map(rowGroup => { + let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1) + return { + ...rowGroup, + [subRowsKey]: subRows, + ...aggregate(subRows) + } + }) + return groupedRows + } + resolvedData = groupRecursively(resolvedData, pivotBy) + } + + return { + resolvedData, + pivotColumn, + allVisibleColumns, + headerGroups, + allDecoratedColumns, + hasHeaderGroups + } + }, + getSortedData (nextProps, nextState) { + const { + manual, + sorting, + allDecoratedColumns, + resolvedData + } = this.getResolvedState(nextProps, nextState) + + const resolvedSorting = sorting.length ? sorting : this.getInitSorting(allDecoratedColumns) + + // Resolve the data from either manual data or sorted data + return { + resolvedSorting, + sortedData: manual ? resolvedData : this.sortData(resolvedData, resolvedSorting) + } + }, + + fireOnChange () { + this.props.onChange(this.getResolvedState(), this) + }, + getPropOrState (key) { + return _.getFirstDefined(this.props[key], this.state[key]) + }, + getStateOrProp (key) { + return _.getFirstDefined(this.state[key], this.props[key]) + }, + getInitSorting (columns) { + if (!columns) { + return [] + } + const initSorting = columns.filter(d => { + return typeof d.sort !== 'undefined' + }).map(d => { + return { + id: d.id, + asc: d.sort === 'asc' + } + }) + + return initSorting + + // return initSorting.length ? initSorting : [{ + // id: columns.find(d => d.id).id, + // asc: true + // }] + }, + sortData (data, sorting) { + const sorted = _.orderBy(data, sorting.map(sort => { + return row => { + if (row[sort.id] === null || row[sort.id] === undefined) { + return -Infinity + } + return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id] + } + }), sorting.map(d => d.asc ? 'asc' : 'desc')) + + return sorted.map(row => { + if (!row[this.props.subRowsKey]) { + return row + } + return { + ...row, + [this.props.subRowsKey]: this.sortData(row[this.props.subRowsKey], sorting) + } + }) + }, + + getMinRows () { + return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize')) + }, + + // User actions + onPageChange (page) { + const { onPageChange } = this.props + if (onPageChange) { + return onPageChange(page) + } + this.setStateWithData({ + expandedRows: {}, + page + }, () => { + this.fireOnChange() + }) + }, + 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) + + if (onPageSizeChange) { + return onPageSizeChange(newPageSize, newPage) + } + + this.setStateWithData({ + pageSize: newPageSize, + page: newPage + }, () => { + this.fireOnChange() + }) + }, + sortColumn (column, additive) { + const { sorting } = this.getResolvedState() + const { onSortingChange } = this.props + if (onSortingChange) { + return onSortingChange(column, additive) + } + let newSorting = _.clone(sorting || []) + if (_.isArray(column)) { + const existingIndex = newSorting.findIndex(d => d.id === column[0].id) + if (existingIndex > -1) { + const existing = newSorting[existingIndex] + if (existing.asc) { + column.forEach((d, i) => { + newSorting[existingIndex + i].asc = false + }) + } else { + if (additive) { + newSorting.splice(existingIndex, column.length) + } else { + column.forEach((d, i) => { + newSorting[existingIndex + i].asc = true + }) + } + } + if (!additive) { + newSorting = newSorting.slice(existingIndex, column.length) + } + } else { + if (additive) { + newSorting = newSorting.concat(column.map(d => ({ + id: d.id, + asc: true + }))) + } else { + newSorting = column.map(d => ({ + id: d.id, + asc: true + })) + } + } + } else { + const existingIndex = newSorting.findIndex(d => d.id === column.id) + if (existingIndex > -1) { + const existing = newSorting[existingIndex] + if (existing.asc) { + existing.asc = false + if (!additive) { + newSorting = [existing] + } + } else { + if (additive) { + newSorting.splice(existingIndex, 1) + } else { + existing.asc = true + newSorting = [existing] + } + } + } else { + if (additive) { + newSorting.push({ + id: column.id, + asc: true + }) + } else { + newSorting = [{ + id: column.id, + asc: true + }] + } + } + } + this.setStateWithData({ + page: ((!sorting.length && newSorting.length) || !additive) ? 0 : this.state.page, + sorting: newSorting + }, () => { + this.fireOnChange() + }) + } +} diff --git a/src/index.js b/src/index.js index 0913509..f17b020 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,11 @@ import classnames from 'classnames' // import _ from './utils' +import componentMethods from './componentMethods' import Pagination from './pagination' +const emptyObj = () => ({}) + export const ReactTableDefaults = { // General data: [], @@ -17,9 +20,9 @@ export const ReactTableDefaults = { expanderColumnWidth: 35, // Controlled State Overrides - // page - // pageSize - // sorting + // page: undefined, + // pageSize: undefined, + // sorting: undefined, // Controlled State Callbacks onExpandSubComponent: undefined, @@ -42,42 +45,43 @@ export const ReactTableDefaults = { // General Callbacks onChange: () => null, - onTrClick: () => null, // Classes className: '', - tableClassName: '', - theadClassName: '', - tbodyClassName: '', - trClassName: '', - trClassCallback: d => null, - thClassName: '', - thGroupClassName: '', - tdClassName: '', - paginationClassName: '', - - // Styles style: {}, - tableStyle: {}, - theadStyle: {}, - tbodyStyle: {}, - trStyle: {}, - trStyleCallback: d => {}, - thStyle: {}, - tdStyle: {}, - paginationStyle: {}, + + // Component decorators + getProps: emptyObj, + getTableProps: emptyObj, + getTheadGroupProps: emptyObj, + getTheadGroupTrProps: emptyObj, + getTheadGroupThProps: emptyObj, + getTheadProps: emptyObj, + getTheadTrProps: emptyObj, + getTheadThProps: emptyObj, + getTbodyProps: emptyObj, + getTrGroupProps: emptyObj, + getTrProps: emptyObj, + getThProps: emptyObj, + getTdProps: emptyObj, + getPaginationProps: emptyObj, + getLoadingProps: emptyObj, // Global Column Defaults column: { sortable: true, show: true, + minWidth: 100, + // Cells only + render: undefined, className: '', style: {}, + getProps: () => ({}), + // Headers only + header: undefined, headerClassName: '', headerStyle: {}, - headerInnerClassName: '', - headerInnerStyle: {}, - minWidth: 100 + getHeaderProps: () => ({}) }, // Text @@ -119,8 +123,14 @@ export const ReactTableDefaults = { PaginationComponent: Pagination, PreviousComponent: undefined, NextComponent: undefined, - LoadingComponent: ({loading, loadingText}) => ( -
+ LoadingComponent: ({className, loading, loadingText, ...rest}) => ( +
{loadingText}
@@ -143,13 +153,13 @@ export default React.createClass({ }, getResolvedState (props, state) { - const resolvedProps = { + const resolvedState = { ...this.state, ...state, ...this.props, ...props } - return resolvedProps + return resolvedState }, componentWillMount () { @@ -206,28 +216,27 @@ export default React.createClass({ }, render () { - const resolvedProps = this.getResolvedState() + const resolvedState = this.getResolvedState() const { children, className, style, - tableClassName, - tableStyle, - theadGroupClassName, - theadStyle, - trClassName, - trStyle, - thClassname, - thStyle, - theadClassName, - tbodyClassName, - tbodyStyle, - onTrClick, - trClassCallback, - trStyleCallback, - tdStyle, + getProps, + getTableProps, + getTheadGroupProps, + getTheadGroupTrProps, + getTheadGroupThProps, + getTheadProps, + getTheadTrProps, + getTheadThProps, + getTbodyProps, + getTrGroupProps, + getTrProps, + getThProps, + getTdProps, + getPaginationProps, + getLoadingProps, showPagination, - paginationClassName, expanderColumnWidth, manual, loadingText, @@ -261,7 +270,7 @@ export default React.createClass({ hasHeaderGroups, // Sorted Data sortedData - } = resolvedProps + } = resolvedState // Determine the flex percentage for each column // const columnPercentage = 100 / allVisibleColumns.length @@ -296,58 +305,115 @@ export default React.createClass({ let rowIndex = -1 - const makeHeaderGroups = () => ( - - { + const theadGroupProps = _.splitProps(getTheadGroupProps(finalState)) + const theadGroupTrProps = _.splitProps(getTheadGroupTrProps(finalState)) + return ( + - {headerGroups.map(makeHeaderGroup)} - - - ) + + {headerGroups.map(makeHeaderGroup)} + + + ) + } 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))) + const theadGroupThProps = _.splitProps(getTheadGroupThProps(finalState, undefined, column)) + const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column)) + + const classes = [ + column.headerClassName, + theadGroupThProps.className, + columnHeaderProps.className + ] + + const styles = { + ...column.headerStyle, + ...theadGroupThProps.style, + ...columnHeaderProps.style + } + + const rest = { + ...theadGroupThProps.rest, + ...columnHeaderProps.rest + } + + const flexStyles = { + flex: `${flex} 0 auto`, + width: `${width}px`, + maxWidth: `${maxWidth}px` + } + if (column.expander) { if (column.pivotColumns) { return ( ) } return ( ) } return ( {typeof column.header === 'function' ? ( { + const theadProps = _.splitProps(getTheadProps(finalState)) + const theadTrProps = _.splitProps(getTheadTrProps(finalState)) return ( {allVisibleColumns.map(makeHeader)} @@ -382,25 +453,47 @@ export default React.createClass({ const show = typeof column.show === 'function' ? column.show() : column.show const width = _.getFirstDefined(column.width, column.minWidth) const maxWidth = _.getFirstDefined(column.width, column.maxWidth) + const theadThProps = _.splitProps(getTheadThProps(finalState, undefined, column)) + const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column)) + + const classes = [ + column.headerClassName, + theadThProps.className, + columnHeaderProps.className + ] + + const styles = { + ...column.headerStyle, + ...theadThProps.style, + ...columnHeaderProps.style + } + + const rest = { + ...theadThProps.rest, + ...columnHeaderProps.rest + } + if (column.expander) { if (column.pivotColumns) { const pivotSort = resolvedSorting.find(d => d.id === column.id) return ( { column.sortable && this.sortColumn(column.pivotColumns, e.shiftKey) }} + {...rest} > {column.pivotColumns.map((pivotColumn, i) => { return ( @@ -422,11 +515,16 @@ export default React.createClass({ } return ( ) } @@ -435,22 +533,21 @@ export default React.createClass({ { column.sortable && this.sortColumn(column, e.shiftKey) }} + {...rest} > {typeof column.header === 'function' ? ( + onTrClick(rowInfo.row, event)} - className={classnames(trClassName, trClassCallback(rowInfo), row._viewIndex % 2 ? '-even' : '-odd')} - style={Object.assign({}, trStyle, trStyleCallback(rowInfo))} + className={classnames( + trProps.className, + row._viewIndex % 2 ? '-even' : '-odd' + )} + style={trProps.style} + {...trProps.rest} > {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) + const tdProps = _.splitProps(getTdProps(finalState, rowInfo, column)) + const columnProps = _.splitProps(column.getProps(finalState, rowInfo, column)) + + const classes = [ + tdProps.className, + column.className, + columnProps.className + ] + + const styles = { + ...tdProps.style, + ...column.style, + ...columnProps.style + } if (column.expander) { const onTdClick = (e) => { @@ -508,13 +627,18 @@ export default React.createClass({ const PivotCell = column.pivotRender return ( {rowInfo.subRows ? ( @@ -543,11 +667,15 @@ export default React.createClass({ // Return the regular expander cell return ( @@ -563,12 +691,17 @@ export default React.createClass({ return ( {typeof Cell === 'function' ? ( { + const trGroupProps = getTrGroupProps(finalState) + const trProps = _.splitProps(getTrProps(finalState)) + const thProps = _.splitProps(getThProps(finalState)) return ( {SubComponent && ( )} {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) + const tdProps = _.splitProps(getTdProps(finalState, undefined, column)) + const columnProps = _.splitProps(column.getProps(finalState, undefined, column)) + + const classes = [ + tdProps.className, + column.className, + columnProps.className + ] + + const styles = { + ...tdProps.style, + ...column.style, + ...columnProps.style + } + return (   @@ -632,457 +797,71 @@ export default React.createClass({ ) } - const makeTable = () => ( -
- { + const rootProps = _.splitProps(getProps(finalState)) + const tableProps = _.splitProps(getTableProps(finalState)) + const tBodyProps = _.splitProps(getTbodyProps(finalState)) + const paginationProps = _.splitProps(getPaginationProps(finalState)) + const loadingProps = getLoadingProps(finalState) + return ( +
- {hasHeaderGroups && makeHeaderGroups()} - {makeHeaders()} - - {pageRows.map((d, i) => makePageRow(d, i))} - {padRows.map(makePadRow)} - - - {showPagination && ( - + {pageRows.map((d, i) => makePageRow(d, i))} + {padRows.map(makePadRow)} + + + {showPagination && ( + + )} + - )} - -
- ) - - // childProps are optionally passed to a function-as-a-child - const childState = { - ...resolvedProps, - startRow, - endRow, - pageRows, - minRows, - padRows, - canPrevious, - canNext, - rowMinWidth +
+ ) } - return children ? children(childState, makeTable, this) : makeTable() + // childProps are optionally passed to a function-as-a-child + return children ? children(finalState, makeTable, this) : makeTable() }, // Helpers - getDataModel (nextProps, nextState) { - const { - columns, - pivotBy = [], - data, - pivotIDKey, - pivotValKey, - subRowsKey, - expanderColumnWidth, - SubComponent - } = this.getResolvedState(nextProps, nextState) + ...componentMethods - // Determine Header Groups - let hasHeaderGroups = false - columns.forEach(column => { - if (column.columns) { - hasHeaderGroups = true - } - }) - - // Build Header Groups - const headerGroups = [] - let currentSpan = [] - - // A convenience function to add a header and reset the currentSpan - 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 = 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) - }) - - // 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) - 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 - let resolvedData = data.map((d, i) => { - const row = { - __original: d, - __index: i - } - allDecoratedColumns.forEach(column => { - if (column.expander) return - row[column.id] = column.accessor(d) - }) - return row - }) - - // 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 - } - let standardColumns = pivotBy.length ? allVisibleColumns.slice(1) : allVisibleColumns - const aggregatingColumns = standardColumns.filter(d => d.aggregate) - let pivotColumn - if (pivotBy.length) { - pivotColumn = allVisibleColumns[0] - 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 - } - } - ) - // Recurse into the subRows - groupedRows = groupedRows.map(rowGroup => { - let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1) - return { - ...rowGroup, - [subRowsKey]: subRows, - ...aggregate(subRows) - } - }) - return groupedRows - } - resolvedData = groupRecursively(resolvedData, pivotBy) - } - - return { - resolvedData, - pivotColumn, - allVisibleColumns, - headerGroups, - allDecoratedColumns, - hasHeaderGroups - } - }, - - getSortedData (nextProps, nextState) { - const { - manual, - sorting, - allDecoratedColumns, - resolvedData - } = this.getResolvedState(nextProps, nextState) - - const resolvedSorting = sorting.length ? sorting : this.getInitSorting(allDecoratedColumns) - - // Resolve the data from either manual data or sorted data - return { - resolvedSorting, - sortedData: manual ? resolvedData : this.sortData(resolvedData, resolvedSorting) - } - }, - - fireOnChange () { - this.props.onChange(this.getResolvedState(), this) - }, - getPropOrState (key) { - return _.getFirstDefined(this.props[key], this.state[key]) - }, - getStateOrProp (key) { - return _.getFirstDefined(this.state[key], this.props[key]) - }, - getInitSorting (columns) { - if (!columns) { - return [] - } - const initSorting = columns.filter(d => { - return typeof d.sort !== 'undefined' - }).map(d => { - return { - id: d.id, - asc: d.sort === 'asc' - } - }) - - 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 => { - return row => { - if (row[sort.id] === null || row[sort.id] === undefined) { - return -Infinity - } - return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id] - } - }), sorting.map(d => d.asc ? 'asc' : 'desc')) - - return sorted.map(row => { - if (!row[this.props.subRowsKey]) { - return row - } - return { - ...row, - [this.props.subRowsKey]: this.sortData(row[this.props.subRowsKey], sorting) - } - }) - }, - - getMinRows () { - return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize')) - }, - - // User actions - onPageChange (page) { - const { onPageChange } = this.props - if (onPageChange) { - return onPageChange(page) - } - this.setStateWithData({ - expandedRows: {}, - page - }, () => { - this.fireOnChange() - }) - }, - 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) - - if (onPageSizeChange) { - return onPageSizeChange(newPageSize, newPage) - } - - this.setStateWithData({ - pageSize: newPageSize, - page: newPage - }, () => { - this.fireOnChange() - }) - }, - sortColumn (column, additive) { - const { sorting } = this.getResolvedState() - const { onSortingChange } = this.props - if (onSortingChange) { - return onSortingChange(column, additive) - } - let newSorting = _.clone(sorting || []) - if (_.isArray(column)) { - const existingIndex = newSorting.findIndex(d => d.id === column[0].id) - if (existingIndex > -1) { - const existing = newSorting[existingIndex] - if (existing.asc) { - column.forEach((d, i) => { - newSorting[existingIndex + i].asc = false - }) - } else { - if (additive) { - newSorting.splice(existingIndex, column.length) - } else { - column.forEach((d, i) => { - newSorting[existingIndex + i].asc = true - }) - } - } - if (!additive) { - newSorting = newSorting.slice(existingIndex, column.length) - } - } else { - if (additive) { - newSorting = newSorting.concat(column.map(d => ({ - id: d.id, - asc: true - }))) - } else { - newSorting = column.map(d => ({ - id: d.id, - asc: true - })) - } - } - } else { - const existingIndex = newSorting.findIndex(d => d.id === column.id) - if (existingIndex > -1) { - const existing = newSorting[existingIndex] - if (existing.asc) { - existing.asc = false - if (!additive) { - newSorting = [existing] - } - } else { - if (additive) { - newSorting.splice(existingIndex, 1) - } else { - existing.asc = true - newSorting = [existing] - } - } - } else { - if (additive) { - newSorting.push({ - id: column.id, - asc: true - }) - } else { - newSorting = [{ - id: column.id, - asc: true - }] - } - } - } - this.setStateWithData({ - page: ((!sorting.length && newSorting.length) || !additive) ? 0 : this.state.page, - sorting: newSorting - }, () => { - this.fireOnChange() - }) - } }) diff --git a/src/utils.js b/src/utils.js index d7b0372..ef18e6b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,16 +6,16 @@ export default { set, takeRight, last, - sortBy, + orderBy, range, remove, clone, getFirstDefined, sum, makeTemplateComponent, - prefixAll, groupBy, - isArray + isArray, + splitProps } function get (obj, path, def) { @@ -61,7 +61,7 @@ function range (n) { return arr } -function sortBy (arr, funcs, dirs) { +function orderBy (arr, funcs, dirs) { return arr.sort((a, b) => { for (let i = 0; i < funcs.length; i++) { const comp = funcs[i] @@ -75,7 +75,9 @@ function sortBy (arr, funcs, dirs) { return desc ? 1 : -1 } } - return 0 + return dirs[0] + ? a.__index - b.__index + : b.__index - b.__index }) } @@ -128,10 +130,6 @@ function makeTemplateComponent (compClass) { ) } -function prefixAll (obj) { - return obj -} - function groupBy (xs, key) { return xs.reduce((rv, x, i) => { const resKey = typeof key === 'function' ? key(x, i) : x[key] @@ -167,3 +165,11 @@ function flattenDeep (arr, newArr = []) { } return newArr } + +function splitProps ({className, style, ...rest}) { + return { + className, + style, + rest + } +}