- {hasHeaderGroups && (
-
-
- {SubComponent && (
-
- )}
- {headerGroups.map((column, i) => {
- return (
- d.minWidth))}px`
- }))}
- >
- {typeof column.header === 'function' ? (
-
- ) : column.header}
-
- )
- })}
-
-
- )}
-
-
- {SubComponent && (
-
- )}
- {decoratedColumns.map((column, i) => {
- const sort = sorting.find(d => d.id === column.id)
- const show = typeof column.show === 'function' ? column.show() : column.show
- return (
- {
- column.sortable && this.sortColumn(column, e.shiftKey)
- }}
- >
- {typeof column.header === 'function' ? (
-
- ) : column.header}
-
- )
- })}
-
-
+ {hasHeaderGroups && makeHeaderGroup()}
+ {makeHeader()}
- {pageRows.map((row, i) => {
- const rowInfo = {
- row: row.__original,
- rowValues: row,
- index: row.__index,
- viewIndex: i
- }
- const visibleSubComponentIndex = visibleSubComponents.indexOf(i)
- const isExpanded = visibleSubComponentIndex > -1
- return (
-
- onTrClick(rowInfo.row, event)}
- className={classnames(trClassName, trClassCallback(rowInfo))}
- style={Object.assign({}, trStyle, trStyleCallback(rowInfo))}
- >
- {SubComponent && (
- {
- if (onExpand) {
- return onExpand(i, e)
- }
- if (isExpanded) {
- return this.setState({
- visibleSubComponents: [
- /* eslint-disable*/
- ...visibleSubComponents.slice(0, visibleSubComponentIndex - 1),
- ...visibleSubComponents.slice(visibleSubComponentIndex + 1)
- /* eslint-enable*/
- ]
- })
- }
- this.setState({
- visibleSubComponents: [
- /* eslint-disable*/
- ...visibleSubComponents,
- i
- /* eslint-enable*/
- ]
- })
- }}
- >
-
-
- )}
- {decoratedColumns.map((column, i2) => {
- const Cell = column.render
- const show = typeof column.show === 'function' ? column.show() : column.show
- return (
-
- {typeof Cell === 'function' ? (
- |
- ) : typeof Cell !== 'undefined' ? Cell
- : rowInfo.rowValues[column.id]}
-
- )
- })}
-
- {SubComponent && isExpanded ? (
- SubComponent(rowInfo)
- ) : null}
-
- )
- })}
- {padRows.map((row, i) => {
- return (
-
- {SubComponent && (
-
- )}
- {decoratedColumns.map((column, i2) => {
- const show = typeof column.show === 'function' ? column.show() : column.show
- return (
-
-
-
- )
- })}
-
- )
- })}
+ {pageRows.map((d, i) => makePageRow(d, i))}
+ {padRows.map(makePadRow)}
{showPagination && (
{
+ 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 = {}) => {
+ headerGroups.push(Object.assign({}, column, {
+ columns: columns
+ }))
+ currentSpan = []
+ }
+
+ // Decorate the columns
+ const decorateAndAddToAll = (col) => {
+ const decoratedColumn = this.makeDecoratedColumn(col)
+ allDecoratedColumns.push(decoratedColumn)
+ return decoratedColumn
+ }
+ let allDecoratedColumns = []
+ const decoratedColumns = columns.map((column, i) => {
+ if (column.columns) {
+ return {
+ ...column,
+ columns: column.columns.map(decorateAndAddToAll)
+ }
+ } else {
+ decorateAndAddToAll(column)
+ }
+ })
+
+ // Build the visible columns and 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)
+ })
+
+ // Build allVisible columns 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)
+ }
+
+ // 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 accessedData = data.map((d, i) => {
+ const row = {
+ __original: d,
+ __index: i
+ }
+ allDecoratedColumns.forEach(column => {
+ 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
+ }
+ accessedData = groupRecursively(accessedData, pivotBy)
+ }
+
+ const resolvedSorting = sorting.length ? sorting : this.getInitSorting(allDecoratedColumns)
+
+ // Resolve the data from either manual data or sorted data
+ const resolvedData = manual ? accessedData : this.sortData(accessedData, resolvedSorting)
+
return {
- ...this.state,
- ...this.props,
- pages: this.getPagesLength(),
- sorting: this.getSorting()
+ columnPercentage,
+ pivotColumn,
+ resolvedData,
+ allVisibleColumns,
+ headerGroups,
+ standardColumns,
+ allDecoratedColumns,
+ hasHeaderGroups,
+ pages: manual ? pages : Math.ceil(resolvedData.length / pageSize)
}
},
+
fireOnChange () {
this.props.onChange(this.getResolvedState(), this)
},
@@ -536,7 +817,7 @@ export default React.createClass({
}]
},
sortData (data, sorting) {
- return _.orderBy(data, sorting.map(sort => {
+ const sorted = _.sortBy(data, sorting.map(sort => {
return row => {
if (row[sort.id] === null || row[sort.id] === undefined) {
return -Infinity
@@ -544,6 +825,16 @@ export default React.createClass({
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)
+ }
+ })
},
makeDecoratedColumn (column) {
const dcol = Object.assign({}, this.props.column, column)
@@ -566,13 +857,6 @@ export default React.createClass({
return dcol
},
- getSorting (columns) {
- return this.props.sorting || (this.state.sorting && this.state.sorting.length ? this.state.sorting : this.getInitSorting(columns))
- },
- getPagesLength () {
- return this.props.manual ? this.props.pages
- : Math.ceil(this.props.data.length / this.getStateOrProp('pageSize'))
- },
getMinRows () {
return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize'))
},
@@ -583,8 +867,8 @@ export default React.createClass({
if (onPageChange) {
return onPageChange(page)
}
- this.setState({
- visibleSubComponents: [],
+ this.setStateWithData({
+ expandedRows: {},
page
}, () => {
this.fireOnChange()
@@ -602,7 +886,7 @@ export default React.createClass({
return onPageSizeChange(newPageSize, newPage)
}
- this.setState({
+ this.setStateWithData({
pageSize: newPageSize,
page: newPage
}, () => {
@@ -610,45 +894,79 @@ export default React.createClass({
})
},
sortColumn (column, additive) {
+ const { sorting } = this.getResolvedState()
const { onSortingChange } = this.props
if (onSortingChange) {
return onSortingChange(column, additive)
}
- const existingSorting = this.getSorting()
- let sorting = _.clone(this.state.sorting || [])
- const existingIndex = sorting.findIndex(d => d.id === column.id)
- if (existingIndex > -1) {
- const existing = sorting[existingIndex]
- if (existing.asc) {
- existing.asc = false
+ 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) {
- sorting = [existing]
+ newSorting = newSorting.slice(existingIndex, column.length)
}
} else {
if (additive) {
- sorting.splice(existingIndex, 1)
+ newSorting = newSorting.concat(column.map(d => ({
+ id: d.id,
+ asc: true
+ })))
} else {
- existing.asc = true
- sorting = [existing]
+ newSorting = column.map(d => ({
+ id: d.id,
+ asc: true
+ }))
}
}
} else {
- if (additive) {
- sorting.push({
- id: column.id,
- asc: true
- })
+ 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 {
- sorting = [{
- id: column.id,
- asc: true
- }]
+ if (additive) {
+ newSorting.push({
+ id: column.id,
+ asc: true
+ })
+ } else {
+ newSorting = [{
+ id: column.id,
+ asc: true
+ }]
+ }
}
}
- const page = (existingIndex === 0 || (!existingSorting.length && sorting.length) || !additive) ? 0 : this.state.page
- this.setState({
- page,
- sorting
+ this.setStateWithData({
+ page: ((!sorting.length && newSorting.length) || !additive) ? 0 : this.state.page,
+ sorting: newSorting
}, () => {
this.fireOnChange()
})
diff --git a/src/index.styl b/src/index.styl
index 2f60d4d..27c5c4e 100644
--- a/src/index.styl
+++ b/src/index.styl
@@ -56,6 +56,8 @@ $expandSize = 7px
border-right:1px solid alpha(black, .02)
&:last-child
border-right:0
+ .rt-pivot
+ cursor: pointer
.rt-tr-group
display: flex
flex-direction: column
@@ -79,29 +81,35 @@ $expandSize = 7px
border:0 !important
opacity: 0 !important
- .rt-expander-wrap
- display:flex
- align-items: center
- justify-content: center
- cursor: pointer
-
.rt-expander
- width: 0
- height: 0
- border-left: ($expandSize * .72) solid transparent
- border-right: ($expandSize * .72) solid transparent
- border-top: $expandSize solid alpha(black, .8)
- transition: all .3s $easeOutBack
- transform: rotate(-90deg)
- &.-open
- transform: rotate(0deg)
+ display: inline-block
+ position:relative
+ margin: 0
+ color: transparent
+ margin: 0 10px
+ &:after
+ content: ''
+ position: absolute
+ width: 0
+ height: 0
+ top:50%
+ left:50%
+ transform: translate(-50%, -50%) rotate(-90deg)
+ border-left: ($expandSize * .72) solid transparent
+ border-right: ($expandSize * .72) solid transparent
+ border-top: $expandSize solid alpha(black, .8)
+ transition: all .3s $easeOutBack
+ cursor: pointer
+ &.-open:after
+ transform: translate(-50%, -50%) rotate(0deg)
&.-striped
- .rt-tr-group:nth-child(even)
+ .rt-tr.-odd
background: alpha(black, .03)
&.-highlight
- .rt-tr-group:hover
- background: alpha(black, .05)
+ .rt-tbody
+ .rt-tr:hover
+ background: alpha(black, .05)
.-pagination
z-index: 1
diff --git a/src/pagination.js b/src/pagination.js
index 8257545..6200ef3 100644
--- a/src/pagination.js
+++ b/src/pagination.js
@@ -17,7 +17,7 @@ export default React.createClass({
this.setState({page: nextProps.page})
},
getSafePage (page) {
- return Math.min(Math.max(page, 0), this.props.pagesLength - 1)
+ return Math.min(Math.max(page, 0), this.props.pages - 1)
},
changePage (page) {
page = this.getSafePage(page)
@@ -32,7 +32,7 @@ export default React.createClass({
render () {
const {
// Computed
- pagesLength,
+ pages,
// Props
page,
showPageSizeOptions,
@@ -85,7 +85,7 @@ export default React.createClass({
) : (
{page + 1}
- )} {this.props.ofText} {pagesLength}
+ )} {this.props.ofText} {pages}
{showPageSizeOptions && (
diff --git a/src/utils.js b/src/utils.js
index c7eeee6..c1a60fa 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -3,41 +3,45 @@ import classnames from 'classnames'
//
export default {
get,
+ set,
takeRight,
last,
- orderBy,
+ sortBy,
range,
- clone,
remove,
+ clone,
getFirstDefined,
sum,
makeTemplateComponent,
- prefixAll
+ prefixAll,
+ groupBy,
+ isArray
}
-function remove (a, b) {
- return a.filter(function (o, i) {
- var r = b(o)
- if (r) {
- a.splice(i, 1)
- return true
- }
- return false
- })
-}
-
-function get (a, b) {
- if (isArray(b)) {
- b = b.join('.')
+function get (obj, path, def) {
+ if (!path) {
+ return obj
}
- return b
- .replace('[', '.').replace(']', '')
- .split('.')
- .reduce(
- function (obj, property) {
- return obj[property]
- }, a
- )
+ const pathObj = makePathArray(path)
+ let val
+ try {
+ val = pathObj.reduce((current, pathPart) => current[pathPart], obj)
+ } catch (e) {}
+ return typeof val !== 'undefined' ? val : def
+}
+
+function set (obj = {}, path, value) {
+ const keys = makePathArray(path)
+ let keyPart
+ let cursor = obj
+ while ((keyPart = keys.shift()) && keys.length) {
+ if (!cursor[keyPart]) {
+ cursor[keyPart] = {}
+ }
+ cursor = cursor[keyPart]
+ }
+ cursor[keyPart] = value
+ return obj
}
function takeRight (arr, n) {
@@ -57,7 +61,7 @@ function range (n) {
return arr
}
-function orderBy (arr, funcs, dirs) {
+function sortBy (arr, funcs, dirs) {
return arr.sort((a, b) => {
for (let i = 0; i < funcs.length; i++) {
const comp = funcs[i]
@@ -75,21 +79,28 @@ function orderBy (arr, funcs, dirs) {
})
}
-function clone (a) {
- return JSON.parse(JSON.stringify(a, function (key, value) {
- if (typeof value === 'function') {
- return value.toString()
+function remove (a, b) {
+ return a.filter(function (o, i) {
+ var r = b(o)
+ if (r) {
+ a.splice(i, 1)
+ return true
}
- return value
- }))
+ return false
+ })
}
-// ########################################################################
-// Helpers
-// ########################################################################
-
-function isArray (a) {
- return Array.isArray(a)
+function clone (a) {
+ try {
+ return JSON.parse(JSON.stringify(a, (key, value) => {
+ if (typeof value === 'function') {
+ return value.toString()
+ }
+ return value
+ }))
+ } catch (e) {
+ return a
+ }
}
function getFirstDefined (...args) {
@@ -120,3 +131,39 @@ 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]
+ rv[resKey] = rv[resKey] || []
+ rv[resKey].push(x)
+ return rv
+ }, {})
+}
+
+function isArray (a) {
+ return Array.isArray(a)
+}
+
+// ########################################################################
+// Non-exported Helpers
+// ########################################################################
+
+function makePathArray (obj) {
+ return flattenDeep(obj)
+ .join('.')
+ .replace('[', '.')
+ .replace(']', '')
+ .split('.')
+}
+
+function flattenDeep (arr, newArr = []) {
+ if (!isArray(arr)) {
+ newArr.push(arr)
+ } else {
+ for (var i = 0; i < arr.length; i++) {
+ flattenDeep(arr[i], newArr)
+ }
+ }
+ return newArr
+}
diff --git a/stories/Pivoting.js b/stories/Pivoting.js
new file mode 100644
index 0000000..2045ea7
--- /dev/null
+++ b/stories/Pivoting.js
@@ -0,0 +1,120 @@
+import React from 'react'
+import _ from 'lodash'
+import namor from 'namor'
+
+import CodeHighlight from './components/codeHighlight'
+import ReactTable from '../lib/index'
+
+export default () => {
+ const data = _.map(_.range(10000), 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 => _.round(_.mean(vals)),
+ render: row => {
+ return {row.aggregated ? `${row.value} (avg)` : row.value}
+ }
+ }, {
+ header: 'Visits',
+ accessor: 'visits',
+ aggregate: vals => _.sum(vals)
+ }]
+ }]
+
+ return (
+
+
+
+
+
+
+ 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'
+ }]
+}]
+
+export default (
+ {
+ return (
+
+
You can put any component you want here, even another React Table!
+
+
+
{
+ return (
+
+ It even has access to the row data:
+ {() => JSON.stringify(row, null, 2)}
+
+ )
+ }}
+ />
+
+ )
+ }}
+ />
+)
+ `
+}
diff --git a/stories/PivotingSubComponents.js b/stories/PivotingSubComponents.js
new file mode 100644
index 0000000..73b9ec4
--- /dev/null
+++ b/stories/PivotingSubComponents.js
@@ -0,0 +1,138 @@
+import React from 'react'
+import _ from 'lodash'
+import namor from 'namor'
+
+import CodeHighlight from './components/codeHighlight'
+import ReactTable from '../lib/index'
+
+export default () => {
+ const data = _.map(_.range(10000), 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 => _.round(_.mean(vals)),
+ render: row => {
+ return {row.aggregated ? `${row.value} (avg)` : row.value}
+ }
+ }, {
+ header: 'Visits',
+ accessor: 'visits',
+ aggregate: vals => _.sum(vals)
+ }]
+ }]
+
+ return (
+
+
+
{
+ return (
+
+
You can put any component you want here, even another React Table!
+
+
+
{
+ 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'
+ }]
+}]
+
+export default (
+ {
+ return (
+
+
You can put any component you want here, even another React Table!
+
+
+
{
+ return (
+
+ It even has access to the row data:
+ {() => JSON.stringify(row, null, 2)}
+
+ )
+ }}
+ />
+
+ )
+ }}
+ />
+)
+ `
+}
diff --git a/stories/ServerSide.js b/stories/ServerSide.js
index 477a25d..4699d63 100644
--- a/stories/ServerSide.js
+++ b/stories/ServerSide.js
@@ -48,6 +48,7 @@ const ServerSide = React.createClass({
}
},
fetchData (state, instance) {
+ console.log(state, instance)
// Whenever the table model changes, or the user sorts or changes pages, this method gets called and passed the current table model.
// You can set the `loading` prop of the table to true to use the built-in one or show you're own loading bar if you want.
this.setState({loading: true})