Enhanced expander and pivot (#215)

* Convert expander column to be more like a regular column.

* Update story.

* Simplify setting render on defaultExpander.

* Add more control over expander and pivot columns using the  column.

* Fix default filtering on pivot column.

* Add comments.
This commit is contained in:
Aaron Schwartz
2017-04-20 16:41:50 -06:00
committed by Tanner Linsley
parent 42b5f5bb18
commit bc7005db99
7 changed files with 341 additions and 171 deletions
+2
View File
@@ -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)
+20 -3
View File
@@ -153,7 +153,6 @@ These are all of the available props (and their default values) for the main `<R
pageSizeOptions: [5, 10, 20, 25, 50, 100],
defaultPageSize: 20,
showPageJump: true,
expanderColumnWidth: 35,
collapseOnSortingChange: true,
collapseOnPageChange: true,
collapseOnDataChange: true,
@@ -258,6 +257,19 @@ These are all of the available props (and their default values) for the main `<R
/>
)
},
// 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
+17 -6
View File
@@ -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 (
<div
className={classnames('rt-expander', isExpanded && '-open')}
{...rest}
>&bull;</div>
<div className={classnames('rt-expander', isExpanded && '-open')}>
&bull;
</div>
)
},
PaginationComponent: Pagination,
+32 -113
View File
@@ -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 (
<ThComponent
key={i}
className={classnames(
'rt-pivot-header',
classes
)}
style={{
...styles,
...flexStyles
}}
{...rest}
/>
)
}
return (
<ThComponent
key={i}
className={classnames(
'rt-expander-header',
classes
)}
style={{
...styles,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
{...rest}
/>
)
}
return (
<ThComponent
key={i}
@@ -372,7 +339,10 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
column: column
})}
{i < column.pivotColumns.length - 1 && (
<ExpanderComponent />
_.normalizeComponent(column.render, {
data: sortedData,
column: column
})
)}
</span>
)
@@ -382,21 +352,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
</ThComponent>
)
}
return (
<ThComponent
key={i}
className={classnames(
'rt-expander-header',
classes
)}
style={{
...styles,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
{...rest}
/>
)
}
return (
@@ -500,7 +455,15 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
</span>
)
if (i < column.pivotColumns.length - 1) {
pivotCols.push(<ExpanderComponent key={col.id + '-' + i} />)
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)) {
</ThComponent>
)
}
return (
<ThComponent
key={i}
className={classnames(
'rt-expander-header',
classes
)}
style={{
...styles,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
{...rest}
/>
)
}
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 ? (
<span>
<ExpanderComponent
isExpanded={isExpanded}
/>
{_.normalizeComponent(column.render, {
...rowInfo,
value: rowInfo.rowValues[column.id],
isExpanded
}, rowInfo.rowValues[column.id])}
{column && column.pivotRender ? (
<PivotCell
{...rowInfo}
@@ -668,37 +622,16 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
</span>
) : SubComponent ? (
<span>
<ExpanderComponent
isExpanded={isExpanded}
/>
{_.normalizeComponent(column.render, {
...rowInfo,
value: rowInfo.rowValues[column.id],
isExpanded
}, rowInfo.rowValues[column.id])}
</span>
) : null}
</TdComponent>
)
}
// Return the regular expander cell
return (
<TdComponent
key={i2}
className={classnames(
classes,
{hidden: !show}
)}
style={{
...styles,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
onClick={onTdClick}
>
<span>
<ExpanderComponent
isExpanded={isExpanded}
/>
</span>
</TdComponent>
)
}
// 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])}
</TdComponent>
)
@@ -859,22 +794,6 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
</TdComponent>
)
}
// Return the regular expander cell
return (
<TdComponent
key={i2}
className={classnames(
classes,
{hidden: !show}
)}
style={{
...styles,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
/>
)
}
// Return regular cell
+51 -27
View File
@@ -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]
+36 -22
View File
@@ -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 <span>{row.aggregated ? '...' : row.value}</span>
}
footer: () => <div style={{textAlign: 'center'}}>First Name</div>
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName
accessor: 'lastName',
footer: () => <div style={{textAlign: 'center'}}>Last Name</div>
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age',
aggregate: vals => _.round(_.mean(vals)),
render: row => {
return <span>{row.aggregated ? `${row.value} (avg)` : row.value}</span>
}
footer: () => <div style={{textAlign: 'center'}}>Age</div>
}]
}, {
expander: true
header: 'Expand',
columns: [{
expander: true,
header: () => (<strong>More</strong>),
width: 65,
render: ({isExpanded, ...rest}) => (
<div>
{isExpanded ? <span>&#x2299;</span> : <span>&#x2295;</span>}
</div>
),
style: {cursor: 'pointer', fontSize: 25, padding: '0', textAlign: 'center', userSelect: 'none'},
footer: () => <span>&hearts;</span>
}]
}]
return (
@@ -71,26 +78,33 @@ const columns = [{
columns: [{
header: 'First Name',
accessor: 'firstName',
render: row => {
return <span>{row.aggregated ? '...' : row.value}</span>
}
footer: () => <div style={{textAlign: 'center'}}>First Name</div>
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName
accessor: 'lastName',
footer: () => <div style={{textAlign: 'center'}}>Last Name</div>
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age',
aggregate: vals => _.round(_.mean(vals)),
render: row => {
return <span>{row.aggregated ? \`$\{row.value} (avg)\` : row.value}</span>
}
footer: () => <div style={{textAlign: 'center'}}>Age</div>
}]
}, {
expander: true
header: 'Expand',
columns: [{
expander: true,
header: () => (<strong>More</strong>),
width: 65,
render: ({isExpanded, ...rest}) => (
<div>
{isExpanded ? <span>&#x2299;</span> : <span>&#x2295;</span>}
</div>
),
style: {cursor: 'pointer', fontSize: 25, padding: '0', textAlign: 'center', userSelect: 'none'},
footer: () => <span>&hearts;</span>
}]
}]
return (
+183
View File
@@ -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 <span>{row.aggregated ? `${row.value} (avg)` : row.value}</span>
}
}, {
header: 'Visits',
accessor: 'visits',
aggregate: vals => _.sum(vals),
hideFilter: true
}]
}, {
header: () => <strong>First &#8674; Last Name Pivot</strong>,
expander: true,
minWidth: 200,
render: ({isExpanded, ...rest}) => (
isExpanded ? <span> &#10136; </span> : <span> &#10137; </span>
),
filterRender: ({key}) => <span key={key}> &#10609; </span>,
footer: () => <div style={{textAlign: 'center'}}><strong>Pivot Footer</strong></div>
}]
return (
<div>
<div className='table-wrap'>
<ReactTable
data={data}
columns={columns}
defaultPageSize={10}
className='-striped -highlight'
pivotBy={['firstName', 'lastName']}
showFilters={true}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>You can put any component you want here, even another React Table!</em>
<br />
<br />
<ReactTable
data={data}
columns={columns.filter(x=>!x.expander)}
defaultPageSize={3}
showPagination={false}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>It even has access to the row data: </em>
<CodeHighlight>{() => JSON.stringify(row, null, 2)}</CodeHighlight>
</div>
)
}}
/>
</div>
)
}}
/>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: Hold shift when sorting to multi-sort!</em>
</div>
<CodeHighlight>{() => getCode()}</CodeHighlight>
</div>
)
}
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 <span>{row.aggregated ? \`\${row.value} (avg)\` : row.value}</span>
}
}, {
header: 'Visits',
accessor: 'visits',
aggregate: vals => _.sum(vals),
hideFilter: true
}]
}, {
header: () => <strong>First &#8674; Last Name Pivot</strong>,
expander: true,
minWidth: 200,
render: ({isExpanded, ...rest}) => (
isExpanded ? <span> &#10136; </span> : <span> &#10137; </span>
),
filterRender: ({key}) => <span key={key}> &#10609; </span>,
footer: () => <div style={{textAlign: 'center'}}><strong>Pivot Footer</strong></div>
}]
return (
<div>
<div className='table-wrap'>
<ReactTable
data={data}
columns={columns}
defaultPageSize={10}
className='-striped -highlight'
pivotBy={['firstName', 'lastName']}
showFilters={true}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>You can put any component you want here, even another React Table!</em>
<br />
<br />
<ReactTable
data={data}
columns={columns.filter(x=>!x.expander)}
defaultPageSize={3}
showPagination={false}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>It even has access to the row data: </em>
<CodeHighlight>{() => JSON.stringify(row, null, 2)}</CodeHighlight>
</div>
)
}}
/>
</div>
)
}}
/>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: Hold shift when sorting to multi-sort!</em>
</div>
<CodeHighlight>{() => getCode()}</CodeHighlight>
</div>
)
`
}