Resizing Fixes, custom sorting, new api names

This commit is contained in:
Tanner Linsley
2017-05-09 01:23:04 -06:00
parent 2d35148c75
commit 307d37421e
19 changed files with 405 additions and 237 deletions

View File

@@ -13,6 +13,7 @@ import Readme from '../README.md'
import Simple from '../stories/Simple.js'
import CellRenderers from '../stories/CellRenderers.js'
import DefaultSorting from '../stories/DefaultSorting.js'
import CustomSorting from '../stories/CustomSorting.js'
import CustomWidths from '../stories/CustomWidths.js'
import CustomComponentProps from '../stories/CustomComponentProps.js'
import ServerSide from '../stories/ServerSide.js'
@@ -46,6 +47,7 @@ configure(() => {
.add('Simple Table', Simple)
.add('Cell Renderers & Custom Components', CellRenderers)
.add('Default Sorting', DefaultSorting)
.add('Custom Sorting', CustomSorting)
.add('Custom Column Widths', CustomWidths)
.add('Custom Component Props', CustomComponentProps)
.add('Server-side Data', ServerSide)

View File

@@ -1,11 +1,27 @@
## 6.0.0
* Pivot Columns can now be sorted and filtered individually.
* More control over Pivot Column rendering.
* Prevent transitions while column resizing for a smoother resize effect.
* Disable text selection while resizing columns.
- Updated/New Renderers:
- `Cell` - deprecates and replaces `render`
- `Header` - deprecates and replaces `header`
- `Footer` - deprecates and replaces `footer`
- `Aggregated` - Custom renderer for aggregated cells
- `Pivot` - Custom renderer for Pivoted Cells (utilizes `Expander` and `PivotValue`)
- `PivotValue` - Custom renderer for Pivot cell values
- `Expander` - Custom renderer for Pivot cell Expander
- `Filter`- Custom renderer for Filters
- Pivot Columns are now visibly separate and sorted/filtered independently.
- Custom sorting methods per instance via `sortMethod` and per column via `column.sortMethod`
- Prevent transitions while column resizing for a smoother resize effect.
- You may now use `column.resizable` to restrict or allow resizing for specific columns
- Disable text selection while resizing columns.
- All callbacks can now be utilized without needing to hoist and manage the piece of state they export. That is what their prop counterparts are for, so now the corresponding prop is used instead of the callback to detect a "fully controlled" state.
- Callbacks now provide the destination state as the primary parameter(s). This makes hoisting and controlling the state in redux or component state much easier. eg.
- `onSorting` no longer requires you to build your own toggle logic
- `onResize` no longer requires you to build your own resize logic
- `onChange` callback is now deprecated in favor of the better labeled `onFetchData` prop, which will always fires when a new data model needs to be fetched (or is build internally).
- `onFilteringChange` has been renamed to `onFilterChange` for simplicity and consistency.
##### Breaking API Changes
* Removed `pivotRender` column option. You can now control how the value is displayed by overriding the `PivotValueComponent`.
* Changed how special `expander: true` column renders pivot columns.
See [Pivoting Options Story](https://react-table.js.org/?selectedKind=2.%20Demos&selectedStory=Pivoting%20Options&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel) for a reference on how to customize pivot column rendering.
* Removed `pivotRender` column option. You can now control how the value is displayed by overriding the `PivotValueComponent` or the individual column's `PivotValue` renderer.
See [Pivoting Options Story](https://react-table.js.org/?selectedKind=2.%20Demos&selectedStory=Pivoting%20Options&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel) for a reference on how to customize pivot column rendering.

149
README.md
View File

@@ -115,18 +115,18 @@ render() {
}]
const columns = [{
header: 'Name',
Header: 'Name',
accessor: 'name' // String-based value accessors!
}, {
header: 'Age',
Header: 'Age',
accessor: 'age',
render: props => <span className='number'>{props.value}</span> // Custom cell components!
Cell: props => <span className='number'>{props.value}</span> // Custom cell components!
}, {
id: 'friendName', // Required because our accessor is not a string
header: 'Friend Name',
Header: 'Friend Name',
accessor: d => d.friend.name // Custom value accessors!
}, {
header: props => <span>Friend Age</span>, // Custom header components!
Header: props => <span>Friend Age</span>, // Custom header components!
accessor: 'friend.age'
}]
@@ -171,14 +171,16 @@ These are all of the available props (and their default values) for the main `<R
page: undefined,
pageSize: undefined,
sorting: undefined,
expandedRows: {},
// Controlled State Callbacks
onExpandSubComponent: undefined,
onPageChange: undefined,
onPageSizeChange: undefined,
onSortingChange: undefined,
onFilteringChange: undefined,
onFilterChange: undefined,
onResize: undefined,
onExpandRow: undefined,
// Pivoting
pivotBy: undefined,
@@ -186,14 +188,8 @@ These are all of the available props (and their default values) for the main `<R
pivotIDKey: '_pivotID',
subRowsKey: '_subRows',
// Pivoting State Overrides (see Fully Controlled Component section)
expandedRows: {},
// Pivoting State Callbacks
onExpandRow: undefined,
// General Callbacks
onChange: () => null,
// Server-side callbacks
onFetchData: () => null,
// Classes
className: '',
@@ -226,50 +222,45 @@ These are all of the available props (and their default values) for the main `<R
// Global Column Defaults
column: {
// Renderers
Cell: undefined,
Header: undefined,
Footer: undefined,
Aggregated: undefined,
Pivot: undefined,
PivotValue: undefined,
Expander: undefined,
Filter: undefined,
// Standard options
resizable: true,
sortable: true,
show: true,
minWidth: 100,
// Cells only
render: undefined,
className: '',
style: {},
getProps: () => ({}),
// Headers only
header: undefined,
headerClassName: '',
headerStyle: {},
getHeaderProps: () => ({})
// Footers only
footer: undefined,
footerClassName: '',
footerStyle: {},
getFooterProps: () => ({}),
filterMethod: undefined,
hideFilter: false,
filterRender: ({filter, onFilterChange}) => (
<input type='text'
style={{
width: '100%'
}}
value={filter ? filter.value : ''}
onChange={(event) => onFilterChange(event.target.value)}
/>
)
hideFilter: false
},
// 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: {
filterRender: undefined
// render: will be overriden in methods.js to display ExpanderComponent and PivotValueComponent
},
pivotDefaults: {},
// Text
previousText: 'Previous',
@@ -309,6 +300,32 @@ Or just define them as props
```javascript
[{
// Renderers
// Provide a JSX element or stateless function to a Renderer to display whatever you want as the column's cell with access to the entire row
// cellInfo: {
// 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
// column: the column properties (id, header, sortable, ...)
// index: the original index of the data supplied to the table
// viewIndex: the index of the row in the current page
// }
Cell: JSX | String | Function | cellInfo => <span>{value}</span>,
Header: JSX | String | Function | cellInfo => <div>Header Name</div>,
Footer: JSX | String | Function | cellInfo => <div>Footer Name</div>,
Filter: JSX | cellInfo => (
<select onChange={event => onFilterChange(event.target.value)} value={filter ? filter.value : ''}></select>
// The value passed to onFilterChange will be the value passed to filter.value of the filterMethod
)
Aggregated: JSX | String | Function | cellInfo => <span>{values.join(',')}</span>,
Pivot: JSX | String | Function | cellInfo => (
<span>
<Expander/><PivotValue />
</span>
),
PivotValue: JSX | String | Function | cellInfo => <div>{value}</div>,
Expander: JSX | String | Function | cellInfo => <MyColumnExpander />,
// General
accessor: 'propertyName', // or Accessor eg. (row) => row.propertyName (see "Accessors" section for more details)
id: 'myProperty', // Conditional - A unique ID is required if the accessor is not a string or if you would like to override the column name used in server-side calls
@@ -319,27 +336,20 @@ Or just define them as props
maxWidth: undefined, // A maximum width for this column.
// Special
// 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.
pivot: false,
// Turns this column into a special column for specifying pivot position in your column definitions.
// The `pivotDefaults` options will be applied on top of this column's options.
// 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)
expander: false,
// Turns this column into a special column for specifying expander position and options in your column definitions.
// The `expanderDefaults` options will be applied on top of this column's options.
// 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,
// the rendering of the expander itself via the `Expander` property
// Cell Options
className: '', // Set the classname of the `td` element of the column
style: {}, // Set the style of the `td` element of the column
render: JSX eg. (cellInfo: {value, rowValues, row, column, index, viewIndex}) => <span>{value}</span>, // 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
// column == the column properties (id, header, sortable, ...)
// index == the original index of the data supplied to the table
// viewIndex == the index of the row in the current page
// Header & HeaderGroup Options
header: 'Header Name', a function that returns a primitive, or JSX / React Component eg. ({data, column}) => <div>Header Name</div>,
headerClassName: '', // Set the classname of the `th` element of the column
headerStyle: {}, // Set the style of the `th` element of the column
getHeaderProps: (state, rowInfo, column, instance) => ({}), // a function that returns props to decorate the `th` element of the column
@@ -348,7 +358,6 @@ Or just define them as props
columns: [...], // See Header Groups section below
// Footer
footer: 'Footer Name' or JSX eg. ({data, column}) => <div>Footer Name</div>,
footerClassName: '', // Set the classname of the `td` element of the column's footer
footerStyle: {}, // Set the style of the `td` element of the column's footer
getFooterProps: (state, rowInfo, column, instance) => ({}), // A function that returns props to decorate the `td` element of the column's footer
@@ -359,7 +368,6 @@ Or just define them as props
// row == the row of data supplied to the table
// column == the column that the filter is on
hideFilter: false, // If `showFilters` is set on the table, this option will let you selectively hide the filter on a particular row
filterRender: JSX // eg. ({filter, onFilterChange}) => <select onChange={event => onFilterChange(event.target.value)} value={filter ? filter.value : ''}></select> // The value passed to onFilterChange will be the value passed to filter.value of the filterMethod
}]
```
@@ -390,16 +398,16 @@ If your data has a field/key with a dot (`.`) you will need to supply a custom a
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',
Header: 'Favorites',
headerClassName: 'my-favorites-column-header-group'
columns: [{
header: 'Color',
Header: 'Color',
accessor: 'favorites.color'
}, {
header: 'Food',
Header: 'Food',
accessor: 'favorites.food'
} {
header: 'Actor',
Header: 'Actor',
accessor: 'favorites.actor'
}]
}]
@@ -420,9 +428,9 @@ You can use any react component or JSX to display content in column headers, cel
// 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: () => <span><i className='fa-tasks' /> Progress</span>,
Header: () => <span><i className='fa-tasks' /> Progress</span>,
accessor: 'progress',
render: row => (
Cell: row => (
<div
style={{
width: '100%',
@@ -565,15 +573,15 @@ Naturally when grouping rows together, you may want to aggregate the rows inside
```javascript
// In this example, we use lodash to sum and average the values, but you can use whatever you want to aggregate.
const columns = [{
header: 'Age',
Header: 'Age',
accessor: 'age',
aggregate: (values, rows) => _.round(_.mean(values)),
render: row => {
Aggregated: row => {
// You can even render the cell differently if it's an aggregated cell
return <span>{row.aggregated ? `${row.value} (avg)` : row.value}</span>
return <span>row.value (avg)</span>
}
}, {
header: 'Visits',
Header: 'Visits',
accessor: 'visits',
aggregate: (values, rows) => _.sum(values)
}]
@@ -604,8 +612,8 @@ If you want to handle pagination, sorting, and filtering on the server, `react-t
1. Feed React Table `data` from somewhere dynamic. eg. `state`, a redux store, etc...
1. Add `manual` as a prop. This informs React Table that you'll be handling sorting and pagination server-side
1. Subscribe to the `onChange` prop. This function is called at `compomentDidMount` and any time sorting or pagination is changed by the user
1. In the `onChange` callback, request your data using the provided information in the params of the function (state and instance)
1. Subscribe to the `onFetchData` prop. This function is called at `compomentDidMount` and any time sorting, pagination or filterting is changed in the table
1. In the `onFetchData` callback, request your data using the provided information in the params of the function (current state and instance)
1. Update your data with the rows to be displayed
1. Optionally set how many pages there are total
@@ -616,7 +624,7 @@ If you want to handle pagination, sorting, and filtering on the server, `react-t
pages={this.state.pages} // should default to -1 (which means we don't know how many pages we have)
loading={this.state.loading}
manual // informs React Table that you'll be handling sorting and pagination server-side
onChange={(state, instance) => {
onFetchData={(state, instance) => {
// show the loading overlay
this.setState({loading: true})
// fetch your data
@@ -664,14 +672,15 @@ Here are the props and their corresponding callbacks that control the state of t
3: true
}
}} // The nested row indexes on the current page that should appear expanded
resizing={[...]} // the current resized column model
// Callbacks
onPageChange={(pageIndex) => {...}} // Called when the page index is changed by the user
onPageSizeChange={(pageSize, pageIndex) => {...}} // Called when the pageSize is changed by the user. The resolve page is also sent to maintain approximate position in the data
onSortingChange={(column, shiftKey) => {...}} // Called when a sortable column header is clicked with the column itself and if the shiftkey was held. If the column is a pivoted column, `column` will be an array of columns
onExpandRow={(index, event) => {...}} // Called when an expander is clicked. Use this to manage `expandedRows`
onFilteringChange={(column, value) => {...}} // Called when a user enters a value into a filter input field or the value passed to the onFilterChange handler by the filterRender option.
onResize={(column, event, isTouch) => {...}} // Called when a user clicks on a resizing component (the right edge of a column header)
onSortingChange={(newSorting, column, shiftKey) => {...}} // Called when a sortable column header is clicked with the column itself and if the shiftkey was held. If the column is a pivoted column, `column` will be an array of columns
onExpandRow={(newExpandedRows, index, event) => {...}} // Called when an expander is clicked. Use this to manage `expandedRows`
onFilterChange={(column, value) => {...}} // Called when a user enters a value into a filter input field or the value passed to the onFilterChange handler by the filterRender option.
onResize={(newResizing, event) => {...}} // Called when a user clicks on a resizing component (the right edge of a column header)
/>
```
@@ -712,6 +721,9 @@ Accessing internal state and wrapping with more UI:
The possibilities are endless!
## Sorting
Sorting comes built in with React-Table. Click column header to sort by its column. Click it again to reverse the sort. You can override the default sorting with the `sortingMethod` prop which takes a column value, and expects you to return a naturally sortable value. You may also override the sorting for individual columns via the `column.sortingMethod` column property.
## Multi-Sort
When clicking on a column header, hold shift to multi-sort! You can toggle `ascending` `descending` and `none` for multi-sort columns. Clicking on a header without holding shift will clear the multi-sort and replace it with the single sort of that column. It's quite handy!
@@ -743,7 +755,10 @@ Object.assign(ReactTableDefaults, {
TdComponent: component,
TfootComponent: component,
ExpanderComponent: component,
AggregateComponent: component,
PivotValueComponent: component,
PivotComponent: component,
FilterComponent: component,
PaginationComponent: component,
PreviousComponent: undefined,
NextComponent: undefined,

View File

@@ -16,7 +16,7 @@
<body>
<div id="root"></div>
<div id="error-display"></div>
<script src="static/preview.5f0262046fdd8adfb130.bundle.js"></script>
<script src="static/preview.b5270b38a18bc77dc351.bundle.js"></script>
</body>
</html>

View File

@@ -38,7 +38,7 @@
</head>
<body style="margin: 0;">
<div id="root"></div>
<script src="static/manager.2e7b063c56eb89efa1fe.bundle.js"></script>
<script src="static/manager.9a120eb2943ac2bf0331.bundle.js"></script>
</body>
</html>

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/manager.2e7b063c56eb89efa1fe.bundle.js","sources":["webpack:///static/manager.2e7b063c56eb89efa1fe.bundle.js"],"mappings":"AAAA;AAkuDA;AA84DA;AA28DA;AA00DA;AAsvEA;AAuhDA;AAsrDA;AAsiDA;AAg6DA;AA2nDA;AA++CA;AAkvDA;AAsnEA;AA2oDA;AAivCA;AA+nDA;AAkpDA;AA6lEA;AAs4DA;AAquDA;AA+pDA;AAoxDA;AAsrDA;AA+yDA;AA88GA;AAj6CA;AAopGA;AAsuFA;AA+zEA;AAtMA;AAizEA","sourceRoot":""}

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/manager.9a120eb2943ac2bf0331.bundle.js","sources":["webpack:///static/manager.9a120eb2943ac2bf0331.bundle.js"],"mappings":"AAAA;AAmuDA;AA64DA;AA28DA;AA00DA;AAsvEA;AAuhDA;AAsrDA;AAsiDA;AAg6DA;AA2nDA;AA++CA;AAkvDA;AAsnEA;AA2oDA;AAivCA;AA+nDA;AAkpDA;AA6lEA;AAs4DA;AAquDA;AA+pDA;AAoxDA;AAsrDA;AAizDA;AA88GA;AAj6CA;AAopGA;AAsuFA;AA+zEA;AAtMA;AAizEA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/preview.5f0262046fdd8adfb130.bundle.js","sources":["webpack:///static/preview.5f0262046fdd8adfb130.bundle.js"],"mappings":"AAAA;AAkuDA;AAmgEA;AA2jFA;AAinFA;AAgsPA;AA+6EA;AAytDA;AA+jEA;AAixDA;AAuzDA;AAk1DA;AA6pDA;AA2+CA;AA+4DA;AAqpDA;AA89CA;AA0nDA;AA2jEA;AA03DA;AA6gCA;AA80DA;AAooDA;AA2nEA;AAw1DA;AA64DA;AAixCA;AAyrFA;AAmjGA;AA4lDA;AA+zCA;AA8vCA;AAshCA;AAwpDA;AA+rCA;AAsCA;AA+jFA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/preview.b5270b38a18bc77dc351.bundle.js","sources":["webpack:///static/preview.b5270b38a18bc77dc351.bundle.js"],"mappings":"AAAA;AAmrDA;AA8uCA;AAqoIA;AA2wGA;AA0pPA;AA65CA;AAk2DA;AA88DA;AAg0DA;AAsgEA;AAmlDA;AAgjDA;AA2hDA;AA6+DA;AAslDA;AA4+CA;AAimDA;AA09DA;AAy4DA;AAwxCA;AAknDA;AA+qDA;AA+nEA;AA4yDA;AAwwCA;AA8tDA;AAuqGA;AA65FA;AAg3CA;AA+1CA;AAkqCA;AAqlCA;AAkgDA;AAo2CA;AAsCA;AA+jFA","sourceRoot":""}

View File

@@ -29,17 +29,19 @@ export default {
resizable: true,
defaultResizing: [],
// Controlled State Overrides
// Controlled State Props
// page: undefined,
// pageSize: undefined,
// sorting: undefined,
// expandedRows: {},
// resizing: [],
// Controlled State Callbacks
onExpandSubComponent: undefined,
onPageChange: undefined,
onPageSizeChange: undefined,
onSortingChange: undefined,
onFilteringChange: undefined,
onFilterChange: undefined,
onResize: undefined,
// Pivoting
@@ -48,14 +50,11 @@ export default {
pivotIDKey: '_pivotID',
subRowsKey: '_subRows',
// Pivoting State Overrides
// expandedRows: {},
// Pivoting State Callbacks
onExpandRow: undefined,
// General Callbacks
onChange: () => null,
// Server-side Callbacks
onFetchData: () => null,
// Classes
className: '',
@@ -115,6 +114,12 @@ export default {
footerStyle: {},
getFooterProps: emptyObj,
filterMethod: undefined,
sortMethod: value => {
if (value === null || value === undefined) {
return -Infinity
}
return typeof value === 'string' ? value.toLowerCase() : value
},
hideFilter: false
},
@@ -126,6 +131,10 @@ export default {
// render: undefined // this is a dynamic default, set at run-time in methods.js to display ExpanderComponent
},
pivotDefaults: {
// extend the defaults for pivoted columns here
},
// Text
previousText: 'Previous',
nextText: 'Next',

View File

@@ -17,7 +17,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
this.getResolvedState = this.getResolvedState.bind(this)
this.getDataModel = this.getDataModel.bind(this)
this.getSortedData = this.getSortedData.bind(this)
this.fireOnChange = this.fireOnChange.bind(this)
this.fireFetchData = this.fireFetchData.bind(this)
this.getPropOrState = this.getPropOrState.bind(this)
this.getStateOrProp = this.getStateOrProp.bind(this)
this.filterData = this.filterData.bind(this)
@@ -108,6 +108,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
ResizerComponent,
ExpanderComponent,
PivotValueComponent,
PivotComponent,
AggregateComponent,
FilterComponent,
// Data model
@@ -127,7 +128,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
const minRows = this.getMinRows()
const padRows = _.range(Math.max(minRows - pageRows.length, 0))
const hasColumnFooter = allVisibleColumns.some(d => d.footer)
const hasColumnFooter = allVisibleColumns.some(d => d.Footer)
const recurseRowsViewIndex = (rows, path = [], index = -1) => {
return [
@@ -298,7 +299,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
...columnHeaderProps.rest
}
const resizer = resizable ? (
const resizer = (typeof column.resizable !== 'undefined' ? column.resizable : resizable) ? (
<ResizerComponent
onMouseDown={e => this.resizeColumnStart(column, e, false)}
onTouchStart={e => this.resizeColumnStart(column, e, true)}
@@ -447,7 +448,11 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
{...trProps.rest}
>
{allVisibleColumns.map((column, i2) => {
const cellInfo = { ...rowInfo, column: {...column} }
const cellInfo = {
...rowInfo,
isExpanded,
column: {...column}
}
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
@@ -455,6 +460,8 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
const tdProps = _.splitProps(getTdProps(finalState, rowInfo, column, this))
const columnProps = _.splitProps(column.getProps(finalState, rowInfo, column, this))
const value = cellInfo.rowValues[column.id]
const classes = [
tdProps.className,
column.className,
@@ -468,26 +475,29 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
}
const onExpanderClick = (e) => {
if (onExpandRow) {
return onExpandRow(cellInfo.nestingPath, e)
}
let newExpandedRows = _.clone(expandedRows)
if (isExpanded) {
return this.setStateWithData({
expandedRows: _.set(newExpandedRows, cellInfo.nestingPath, false)
})
newExpandedRows = _.set(newExpandedRows, cellInfo.nestingPath, false)
} else {
newExpandedRows = _.set(newExpandedRows, cellInfo.nestingPath, {})
}
if (onExpandRow) {
onExpandRow(newExpandedRows, cellInfo.nestingPath, e)
}
// If expandedRows is being controlled, don't manage internal state
if (this.props.expandedRows) {
return
}
return this.setStateWithData({
expandedRows: _.set(newExpandedRows, cellInfo.nestingPath, {})
expandedRows: newExpandedRows
})
}
// Default to a standard cell
let resolvedCell = _.normalizeComponent(column.Cell, {
...cellInfo,
value: cellInfo.rowValues[column.id],
isExpanded
}, cellInfo.rowValues[column.id])
value
}, value)
let interactionProps
let isBranch
@@ -498,6 +508,9 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
const ResolvedAggregateComponent = column.Aggregated ||
!column.aggregate && AggregateComponent
// Resolve ExpanderComponent
const ResolvedExpanderComponent = column.Expander || ExpanderComponent
// Is this column pivoted?
if (pivotBy && column.pivot) {
// Make it expandable
@@ -513,15 +526,14 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
cellInfo.subRows
// Resolve Pivot Renderers
const ResolvedExpanderComponent = column.Expander || ExpanderComponent
const ResolvedPivotValueComponent = column.PivotValue || PivotValueComponent
// Build the default PivotComponent
const DefaultResolvedPivotComponent = props => (
const DefaultResolvedPivotComponent = PivotComponent || (props => (
<div>
<ResolvedExpanderComponent {...props} />
<ResolvedPivotValueComponent {...props} />
</div>
)
))
// Allow a completely custom pivotRender
const resolvedPivot = column.Pivot || DefaultResolvedPivotComponent
// Pivot Cell Render Override
@@ -529,29 +541,30 @@ export default class ReactTable extends Methods(Lifecycle(Component)) {
// isPivot
resolvedCell = _.normalizeComponent(resolvedPivot, {
...cellInfo,
value: row[pivotValKey],
isExpanded
}, cellInfo.rowValues[column.id])
value: row[pivotValKey]
}, row[pivotValKey])
} else if (isPreview) {
// Show the pivot preview
resolvedCell = _.normalizeComponent(ResolvedAggregateComponent, {
...cellInfo,
value: cellInfo.rowValues[column.id],
isExpanded
}, cellInfo.rowValues[column.id])
value
}, value)
} else {
resolvedCell = null
}
} else if (cellInfo.aggregated) {
resolvedCell = _.normalizeComponent(ResolvedAggregateComponent, {
...cellInfo,
value: cellInfo.rowValues[column.id],
isExpanded
}, cellInfo.rowValues[column.id])
value
}, value)
}
// Expander onClick event
if (column.expander) {
resolvedCell = _.normalizeComponent(ResolvedExpanderComponent, {
...cellInfo,
value
})
expandable = true
interactionProps = {
onClick: onExpanderClick

View File

@@ -5,7 +5,7 @@ export default Base => class extends Base {
}
componentDidMount () {
this.fireOnChange()
this.fireFetchData()
}
componentWillReceiveProps (nextProps, nextState) {

View File

@@ -31,20 +31,6 @@ export default Base => class extends Base {
}
})
// 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({
...this.props.column,
...column,
columns: columns
})
currentSpan = []
}
let columnsWithExpander = [...columns]
let expanderColumn = columns.find(col => col.expander || (col.columns && col.columns.some(col2 => col2.expander)))
@@ -64,7 +50,7 @@ export default Base => class extends Base {
if (column.expander) {
dcol = {
...this.props.column,
render: this.props.ExpanderComponent,
Header: this.props.ExpanderComponent,
...this.props.expanderDefaults,
...column
}
@@ -153,6 +139,7 @@ export default Base => class extends Base {
let pivotColumnGroup = {
header: () => <strong>Group</strong>,
columns: pivotColumns.map(col => ({
...this.props.pivotDefaults,
...col,
pivot: true
}))
@@ -170,6 +157,20 @@ export default Base => class extends Base {
}
}
// Build Header Groups
const headerGroups = []
let currentSpan = []
// A convenience function to add a header and reset the currentSpan
const addHeader = (columns, column) => {
headerGroups.push({
...this.props.column,
...column,
columns: columns
})
currentSpan = []
}
// Build flast list of allVisibleColumns and HeaderGroups
visibleColumns.forEach((column, i) => {
if (column.columns) {
@@ -261,17 +262,36 @@ export default Base => class extends Base {
showFilters,
defaultFilterMethod,
resolvedData,
allVisibleColumns
allVisibleColumns,
allDecoratedColumns
} = resolvedState
const sortersByID = {}
allDecoratedColumns
.filter(col => col.sortMethod)
.forEach(col => {
sortersByID[col.id] = col.sortMethod
})
// Resolve the data from either manual data or sorted data
return {
sortedData: manual ? resolvedData : this.sortData(this.filterData(resolvedData, showFilters, filtering, defaultFilterMethod, allVisibleColumns), sorting)
sortedData: manual ? resolvedData : this.sortData(
this.filterData(
resolvedData,
showFilters,
filtering,
defaultFilterMethod,
allVisibleColumns
),
sorting,
sortersByID
)
}
}
fireOnChange () {
this.props.onChange(this.getResolvedState(), this)
fireFetchData () {
this.props.onFetchData(this.getResolvedState(), this)
}
getPropOrState (key) {
@@ -333,17 +353,20 @@ export default Base => class extends Base {
return filteredData
}
sortData (data, sorting) {
sortData (data, sorting, sortersByID = {}) {
if (!sorting.length) {
return data
}
const sorted = _.orderBy(data, sorting.map(sort => {
return row => {
if (row[sort.id] === null || row[sort.id] === undefined) {
return -Infinity
// Support custom sorting methods for each column
if (sortersByID[sort.id]) {
return row => {
return sortersByID[sort.id](row[sort.id])
}
return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id]
}
return row => {
return this.props.sortMethod[sort.id](row[sort.id])
}
}), sorting.map(d => !d.desc))
@@ -353,7 +376,7 @@ export default Base => class extends Base {
}
return {
...row,
[this.props.subRowsKey]: this.sortData(row[this.props.subRowsKey], sorting)
[this.props.subRowsKey]: this.sortData(row[this.props.subRowsKey], sorting, sortersByID)
}
})
}
@@ -365,18 +388,19 @@ export default Base => class extends Base {
// User actions
onPageChange (page) {
const {onPageChange, collapseOnPageChange} = this.props
if (onPageChange) {
return onPageChange(page)
onPageChange && onPageChange(page)
// If controlled, do not keep track of state
if (typeof this.props.page !== 'undefined') {
this.fireFetchData()
return
}
const newState = {page}
if (collapseOnPageChange) {
newState.expandedRows = {}
}
this.setStateWithData(
newState
, () => {
this.fireOnChange()
})
this.setStateWithData(newState, () => {
this.fireFetchData()
})
}
onPageSizeChange (newPageSize) {
@@ -387,15 +411,17 @@ export default Base => class extends Base {
const currentRow = pageSize * page
const newPage = Math.floor(currentRow / newPageSize)
if (onPageSizeChange) {
return onPageSizeChange(newPageSize, newPage)
onPageSizeChange && onPageSizeChange(newPageSize, newPage)
if (typeof this.props.page !== 'undefined') {
this.fireFetchData()
return
}
this.setStateWithData({
pageSize: newPageSize,
page: newPage
}, () => {
this.fireOnChange()
this.fireFetchData()
})
}
@@ -414,9 +440,7 @@ export default Base => class extends Base {
}
const {onSortingChange} = this.props
if (onSortingChange) {
return onSortingChange(column, additive)
}
let newSorting = _.clone(sorting || []).map(d => {
d.desc = _.isSortingDesc(d)
return d
@@ -489,20 +513,26 @@ export default Base => class extends Base {
}
}
}
// If controlled, do not keep track of state
onSortingChange && onSortingChange(newSorting, column, additive)
if (typeof this.props.sorting !== 'undefined') {
this.fireFetchData()
return
}
this.setStateWithData({
page: ((!sorting.length && newSorting.length) || !additive) ? 0 : this.state.page,
sorting: newSorting
}, () => {
this.fireOnChange()
this.fireFetchData()
})
}
filterColumn (column, value) {
const {filtering} = this.getResolvedState()
const {onFilteringChange} = this.props
const {onFilterChange} = this.props
if (onFilteringChange) {
return onFilteringChange(column, value)
if (onFilterChange) {
return onFilterChange(column, value)
}
// Remove old filter first if it exists
@@ -522,17 +552,11 @@ export default Base => class extends Base {
this.setStateWithData({
filtering: newFiltering
}, () => {
this.fireOnChange()
this.fireFetchData()
})
}
resizeColumnStart (column, event, isTouch) {
const {onResize} = this.props
if (onResize) {
return onResize(column, event, isTouch)
}
const parentWidth = event.target.parentElement.getBoundingClientRect().width
let pageX
@@ -588,6 +612,7 @@ export default Base => class extends Base {
}
resizeColumnMoving (event) {
const {onResize} = this.props
const {resizing, currentlyResizing} = this.getResolvedState()
// Delete old value
@@ -609,6 +634,12 @@ export default Base => class extends Base {
value: newWidth
})
onResize && onResize(newResizing, event)
if (this.props.resizing) {
return
}
this.setStateWithData({
resizing: newResizing
})

View File

@@ -34,29 +34,14 @@ const columns = [{
class ControlledTable extends React.Component {
constructor () {
super()
this.sortChange = this.sortChange.bind(this)
this.state = {
sorting: [],
page: 0,
pageSize: 10
pageSize: 10,
expandedRows: {},
resizing: []
}
}
sortChange (column, shift) {
if (shift) {
window.alert('Shift click not implemented in this demo')
}
var sort = {id: column.id}
if (this.state.sorting.length && this.state.sorting[0].id === column.id) {
this.state.sorting[0].asc ? sort.desc = true : sort.asc = true
} else {
sort.asc = true
}
this.setState({
sorting: [sort]
})
}
render () {
return (
<div>
@@ -66,11 +51,16 @@ class ControlledTable extends React.Component {
data={data}
columns={columns}
sorting={this.state.sorting}
onSortingChange={this.sortChange}
onSortingChange={sorting => this.setState({sorting})}
page={this.state.page}
onPageChange={page => this.setState({page})}
pageSize={this.state.pageSize}
onPageSizeChange={(pageSize, page) => this.setState({page, pageSize})}
expandedRows={this.state.expandedRows}
onExpandRow={(expandedRows) => this.setState({expandedRows})}
resizing={this.state.resizing}
onResize={resizing => this.setState({resizing})}
pivotBy={['lastName']}
/>
</div>
<div style={{textAlign: 'center'}}>

93
stories/CustomSorting.js Normal file
View File

@@ -0,0 +1,93 @@
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 (Sorted by Length)',
accessor: 'firstName',
sortMethod: value => {
return value.length
}
}, {
Header: 'Last Name (Sorted by last letter)',
id: 'lastName',
accessor: d => d.lastName,
sortMethod: value => {
return value.substring([value.length - 1])
}
}]
}, {
Header: 'Info',
columns: [{
Header: 'Age',
accessor: 'age'
}]
}]
return (
<div>
<div className='table-wrap'>
<ReactTable
className='-striped -highlight'
data={data}
columns={columns}
defaultPageSize={10}
/>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: Hold shift when sorting to multi-sort!</em>
</div>
<CodeHighlight>{() => getCode()}</CodeHighlight>
</div>
)
}
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 (
<ReactTable
className='-striped -highlight'
data={data}
columns={columns}
defaultPageSize={10}
/>
)
`
}

View File

@@ -65,7 +65,6 @@ const ServerSide = React.createClass({
// Request the data however you want. Here, we'll use our mocked service we created earlier
requestData(state.pageSize, state.page, state.sorting, state.filtering)
.then((res) => {
console.log(res.rows)
// Now just get the rows of data to your React Table (and update anything else like total pages or loading)
this.setState({
data: res.rows,
@@ -97,7 +96,7 @@ const ServerSide = React.createClass({
data={this.state.data} // Set the rows to be displayed
pages={this.state.pages} // Display the total number of pages
loading={this.state.loading} // Display the loading overlay when we need it
onChange={this.fetchData} // Request new data when things change
onFetchData={this.fetchData} // Request new data when things change
/>
</div>
<div style={{textAlign: 'center'}}>
@@ -168,7 +167,7 @@ export default React.createClass({
data={this.state.data} // Set the rows to be displayed
pages={this.state.pages} // Display the total number of pages
loading={this.state.loading} // Display the loading overlay when we need it
onChange={this.fetchData} // Request new data when things change
onFetchData={this.fetchData} // Request new data when things change
/>
}
})