diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d3fa7..41900d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +## 6.7.4 +#### Fixes & Optimizations +- Fix Prop types for columns + +## 6.7.3 +#### Fixes & Optimizations +- Fix the rest of the proptypes + +## 6.7.2 +#### Fixes & Optimizations +- `getPropTypes` proptype check + +## 6.7.1 +#### Fixes & Optimizations +- `eslint-config` moved to dev deps + +## 6.7.0 +## 6.7.0-alpha-0 +#### New Features +- Expose page/pageSize to rows/cells +- Supply sort direction to custom sort methods + +#### Fixes & Optimizations +- README updates +- Linter cleanup +- Added PropTypes node module +- Deps, linting and style upgrades + +## 6.6.0 +#### Fixes & Optimizations +- moved repo to react-tools +- Doc examples moved to codesandbox.io +- README updates +- CSS refacting for rt-tfoot to match rt-thead +- CSS more specific for input and select + +## 6.5.3 +#### Fixes & Optimizations +- `onClick` proxying and eslint + +## 6.5.2 +#### New Features +- Provide onClick handleOriginal function - #406 + +#### Fixes & Optimizations +- README updates +- `makePathArray` in utils - #326 +- Various fixes: #294, #376, #398, #415, + +## 6.5.1 +#### Fixes & Optimizations +- `defaultExpanded` now works correctly - #372 +- `column.getProps().rest` props are now applied correctly +- `makeTemplateComponent` now supports `displayName` - #289 + ## 6.5.0 ##### New Features - `column.filterAll` - defaults to `false`, but when set to `true` will provide the entire array of rows to `filterMethod` as opposed to one row at a time. This allows for more fine-grained filtering using any method you can dream up. See the [Custom Filtering example](https://react-table.js.org/#/story/custom-filtering) for more info. diff --git a/README.md b/README.md index 7778e92..93ce030 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ These are all of the available props (and their default values) for the main ` + • + + ) + } +} + +class SubComponent extends React.Component { + render() + { + return
Nothing
+ } +} + +export default class ComponentTest extends React.Component { + render() + { + const rtProps = { + data, + columns, + ExpanderComponent: (props)=>, + SubComponent: (props)=>, + multiSort: false, + } + return ( + + ) + } +} diff --git a/docs/src/examples/index.js b/docs/src/examples/index.js index ffcd74d..dd9864d 100644 --- a/docs/src/examples/index.js +++ b/docs/src/examples/index.js @@ -1,8 +1,11 @@ +/* eslint-disable */ -import TreeTable from './treetable'; -import CheckboxTable from './checkbox'; +import TreeTable from './treetable' +import SelectTable from './selecttable' +import SelectTreeTable from './selecttreetable' export { TreeTable, - CheckboxTable, + SelectTable, + SelectTreeTable, } diff --git a/docs/src/examples/selecttable/index.js b/docs/src/examples/selecttable/index.js new file mode 100644 index 0000000..4fc0007 --- /dev/null +++ b/docs/src/examples/selecttable/index.js @@ -0,0 +1,174 @@ + +import React from 'react'; +import shortid from 'shortid'; + +import ReactTable from '../../../../lib/index' +import '../../../../react-table.css' + +import selectTableHOC from '../../../../lib/hoc/selectTable' + +const SelectTable = selectTableHOC(ReactTable); + +async function getData() +{ + const result = await ( await fetch('/au_500_tree.json') ).json(); + // we are adding a unique ID to the data for tracking the selected records + return result.map((item)=>{ + const _id = shortid.generate(); + return { + _id, + ...item, + } + }); +} + +function getColumns(data) +{ + const columns = []; + const sample = data[0]; + for(let key in sample) + { + if(key==='_id') continue; + columns.push({ + accessor: key, + Header: key, + }) + } + return columns; +} + +export class ComponentTest extends React.Component { + constructor(props) { + super(props); + this.state = + { + data: null, + columns: null, + selection: [], + selectAll: false, + selectType: 'checkbox', + }; + } + componentDidMount() + { + getData().then((data)=>{ + const columns = getColumns(data); + this.setState({ data, columns }); + }); + } + toggleSelection = (key,shift,row) => { + /* + Implementation of how to manage the selection state is up to the developer. + This implementation uses an array stored in the component state. + Other implementations could use object keys, a Javascript Set, or Redux... etc. + */ + // start off with the existing state + if (this.state.selectType === 'radio') { + let selection = []; + if (selection.indexOf(key)<0) selection.push(key); + this.setState({selection}); + } else { + let selection = [ + ...this.state.selection + ]; + const keyIndex = selection.indexOf(key); + // check to see if the key exists + if(keyIndex>=0) { + // it does exist so we will remove it using destructing + selection = [ + ...selection.slice(0,keyIndex), + ...selection.slice(keyIndex+1) + ] + } else { + // it does not exist so add it + selection.push(key); + } + // update the state + this.setState({selection}); + } + } + toggleAll = () => { + /* + 'toggleAll' is a tricky concept with any filterable table + do you just select ALL the records that are in your data? + OR + do you only select ALL the records that are in the current filtered data? + + The latter makes more sense because 'selection' is a visual thing for the user. + This is especially true if you are going to implement a set of external functions + that act on the selected information (you would not want to DELETE the wrong thing!). + + So, to that end, access to the internals of ReactTable are required to get what is + currently visible in the table (either on the current page or any other page). + + The HOC provides a method call 'getWrappedInstance' to get a ref to the wrapped + ReactTable and then get the internal state and the 'sortedData'. + That can then be iterrated to get all the currently visible records and set + the selection state. + */ + const selectAll = this.state.selectAll?false:true; + const selection = []; + if(selectAll) + { + // we need to get at the internals of ReactTable + const wrappedInstance = this.selectTable.getWrappedInstance(); + // the 'sortedData' property contains the currently accessible records based on the filter and sort + const currentRecords = wrappedInstance.getResolvedState().sortedData; + // we just push all the IDs onto the selection array + currentRecords.forEach((item)=>{ + if(item._original) + { + selection.push(item._original._id); + } + }) + } + this.setState({selectAll,selection}) + } + isSelected = (key) => { + /* + Instead of passing our external selection state we provide an 'isSelected' + callback and detect the selection state ourselves. This allows any implementation + for selection (either an array, object keys, or even a Javascript Set object). + */ + return this.state.selection.includes(key); + } + logSelection = () => { + console.log('selection:',this.state.selection); + } + toggleType = () => { + this.setState({ selectType: this.state.selectType === 'radio' ? 'checkbox' : 'radio', selection: [], selectAll: false, }); + } + render(){ + const { toggleSelection, toggleAll, isSelected, logSelection, toggleType } = this; + const { data, columns, selectAll, selectType } = this.state; + const extraProps = + { + selectAll, + isSelected, + toggleAll, + toggleSelection, + selectType, + } + return ( +
+

react-table - Select Table

+ + + {` (${this.state.selection.length}) selected`} + { + data? + this.selectTable = r} + className="-striped -highlight" + {...extraProps} + /> + :null + } +
+ ); + } +} + +export default ComponentTest; diff --git a/docs/src/examples/selecttreetable/index.js b/docs/src/examples/selecttreetable/index.js new file mode 100644 index 0000000..4766777 --- /dev/null +++ b/docs/src/examples/selecttreetable/index.js @@ -0,0 +1,216 @@ + +import React from 'react'; +import shortid from 'shortid'; + +import ReactTable from '../../../../lib/index' +import '../../../../react-table.css' + +import selectTableHOC from '../../../../lib/hoc/selectTable' +import treeTableHOC from '../../../../lib/hoc/treeTable' + +const SelectTreeTable = selectTableHOC(treeTableHOC(ReactTable)); + +async function getData() +{ + const result = await ( await fetch('/au_500_tree.json') ).json(); + // we are adding a unique ID to the data for tracking the selected records + return result.map((item)=>{ + const _id = shortid.generate(); + return { + _id, + ...item, + } + }); +} + +function getColumns(data) +{ + const columns = []; + const sample = data[0]; + for(let key in sample) + { + if(key==='_id') continue; + columns.push({ + accessor: key, + Header: key, + }) + } + return columns; +} + +function getNodes(data,node=[]) +{ + data.forEach((item)=>{ + if(item.hasOwnProperty('_subRows') && item._subRows) + { + node = getNodes(item._subRows,node); + } else { + node.push(item._original); + } + }); + return node; +} + +export class ComponentTest extends React.Component { + constructor(props) { + super(props); + this.state = + { + data: null, + columns: null, + selection: [], + selectAll: false, + selectType: 'checkbox', + }; + } + componentDidMount() + { + getData().then((data)=>{ + const columns = getColumns(data); + const pivotBy = ['state','post']; + this.setState({ data, columns, pivotBy }); + }); + } + toggleSelection = (key,shift,row) => { + /* + Implementation of how to manage the selection state is up to the developer. + This implementation uses an array stored in the component state. + Other implementations could use object keys, a Javascript Set, or Redux... etc. + */ + // start off with the existing state + if (this.state.selectType === 'radio') { + let selection = []; + if (selection.indexOf(key)<0) selection.push(key); + this.setState({selection}); + } else { + let selection = [ + ...this.state.selection + ]; + const keyIndex = selection.indexOf(key); + // check to see if the key exists + if(keyIndex>=0) { + // it does exist so we will remove it using destructing + selection = [ + ...selection.slice(0,keyIndex), + ...selection.slice(keyIndex+1) + ] + } else { + // it does not exist so add it + selection.push(key); + } + // update the state + this.setState({selection}); + } + } + toggleAll = () => { + /* + 'toggleAll' is a tricky concept with any filterable table + do you just select ALL the records that are in your data? + OR + do you only select ALL the records that are in the current filtered data? + + The latter makes more sense because 'selection' is a visual thing for the user. + This is especially true if you are going to implement a set of external functions + that act on the selected information (you would not want to DELETE the wrong thing!). + + So, to that end, access to the internals of ReactTable are required to get what is + currently visible in the table (either on the current page or any other page). + + The HOC provides a method call 'getWrappedInstance' to get a ref to the wrapped + ReactTable and then get the internal state and the 'sortedData'. + That can then be iterrated to get all the currently visible records and set + the selection state. + */ + const selectAll = this.state.selectAll?false:true; + const selection = []; + if(selectAll) + { + // we need to get at the internals of ReactTable + const wrappedInstance = this.selectTable.getWrappedInstance(); + // the 'sortedData' property contains the currently accessible records based on the filter and sort + const currentRecords = wrappedInstance.getResolvedState().sortedData; + // we need to get all the 'real' (original) records out to get at their IDs + const nodes = getNodes(currentRecords); + // we just push all the IDs onto the selection array + nodes.forEach((item)=>{ + selection.push(item._id); + }) + } + this.setState({selectAll,selection}) + } + isSelected = (key) => { + /* + Instead of passing our external selection state we provide an 'isSelected' + callback and detect the selection state ourselves. This allows any implementation + for selection (either an array, object keys, or even a Javascript Set object). + */ + return this.state.selection.includes(key); + } + logSelection = () => { + console.log('selection:',this.state.selection); + } + toggleType = () => { + this.setState({ selectType: this.state.selectType === 'radio' ? 'checkbox' : 'radio', selection: [], selectAll: false, }); + } + toggleTree = () => { + if(this.state.pivotBy.length) { + this.setState({pivotBy:[],expanded:{}}); + } else { + this.setState({pivotBy:['state','post'],expanded:{}}); + } + } + onExpandedChange = (expanded) => { + this.setState({expanded}); + } + render(){ + const { toggleSelection, toggleAll, isSelected, logSelection, toggleType, toggleTree, onExpandedChange, } = this; + const { data, columns, selectAll, selectType, pivotBy, expanded, } = this.state; + const extraProps = + { + selectAll, + isSelected, + toggleAll, + toggleSelection, + selectType, + pivotBy, + expanded, + onExpandedChange, + pageSize: 5, + } + return ( +
+

react-table - Select Tree Table

+

This example combines two HOCs (the TreeTable and the SelectTable) to make a composite component.

+

We'll call it SelectTreeTable!

+

Here is what the buttons do:

+
    +
  • Toggle Tree: enables or disabled the pivotBy on the table.
  • +
  • Select Type: changes from 'checkbox' to 'radio' and back again.
  • +
  • Log Selection to Console: open your console to see what has been selected.
  • +
+

+ NOTE: the selection is maintained when toggling the tree on and off but is cleared + when switching between select types (radio, checkbox). +

+ + + + {` (${this.state.selection.length}) selected`} + { + data? + this.selectTable = r} + className="-striped -highlight" + freezeWhenExpanded={true} + {...extraProps} + /> + :null + } +
+ ); + } +} + +export default ComponentTest; diff --git a/docs/src/examples/treetable/index.js b/docs/src/examples/treetable/index.js index 34ecc51..dc87fa6 100644 --- a/docs/src/examples/treetable/index.js +++ b/docs/src/examples/treetable/index.js @@ -4,7 +4,7 @@ import React from 'react'; import ReactTable from '../../../../lib/index' import '../../../../react-table.css' -import treeTableHOC from './treeTableHOC'; +import treeTableHOC from '../../../../lib/hoc/treeTable' async function getData() { diff --git a/docs/src/stories/HOCReadme.js b/docs/src/stories/HOCReadme.js new file mode 100644 index 0000000..648091d --- /dev/null +++ b/docs/src/stories/HOCReadme.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +import React from 'react' +import marked from 'marked' +// +import HOCReadme from '!raw!../../../src/hoc/README.md' +import 'github-markdown-css/github-markdown.css' +import './utils/prism.js' + +export default class HOCStory extends React.Component { + render () { + return ( +
+ +
+ ) + } + componentDidMount () { + global.Prism && global.Prism.highlightAll() + } +} diff --git a/docs/yarn.lock b/docs/yarn.lock index 4345bba..5cbc695 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2417,7 +2417,7 @@ eslint-config-standard-jsx@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-4.0.2.tgz#009e53c4ddb1e9ee70b4650ffe63a7f39f8836e1" -eslint-config-standard@10.2.1: +eslint-config-standard@10.2.1, eslint-config-standard@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz#c061e4d066f379dc17cd562c64e819b4dd454591" @@ -2492,7 +2492,7 @@ eslint-plugin-import@2.0.1: minimatch "^3.0.3" pkg-up "^1.0.0" -eslint-plugin-import@^2.7.0: +eslint-plugin-import@^2.7.0, eslint-plugin-import@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894" dependencies: @@ -2545,6 +2545,15 @@ eslint-plugin-jsx-a11y@^6.0.2: emoji-regex "^6.1.0" jsx-ast-utils "^1.4.0" +eslint-plugin-node@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz#80df3253c4d7901045ec87fa660a284e32bdca29" + dependencies: + ignore "^3.3.6" + minimatch "^3.0.4" + resolve "^1.3.3" + semver "5.3.0" + eslint-plugin-node@~4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-4.2.2.tgz#82959ca9aed79fcbd28bb1b188d05cac04fb3363" @@ -2555,6 +2564,10 @@ eslint-plugin-node@~4.2.2: resolve "^1.1.7" semver "5.3.0" +eslint-plugin-promise@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.6.0.tgz#54b7658c8f454813dc2a870aff8152ec4969ba75" + eslint-plugin-promise@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca" @@ -2585,7 +2598,7 @@ eslint-plugin-react@~6.10.0: jsx-ast-utils "^1.3.4" object.assign "^4.0.4" -eslint-plugin-standard@~3.0.1: +eslint-plugin-standard@^3.0.1, eslint-plugin-standard@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-3.0.1.tgz#34d0c915b45edc6f010393c7eef3823b08565cf2" @@ -3479,6 +3492,10 @@ html-element-attributes@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-1.2.0.tgz#8b1c7aaf94353fd9b455c27ec7ebaf1583e29fd0" +html-element-attributes@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-1.3.0.tgz#f06ebdfce22de979db82020265cac541fb17d4fc" + html-encoding-sniffer@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -3612,6 +3629,10 @@ ignore@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d" +ignore@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6113,6 +6134,12 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.2.0: dependencies: path-parse "^1.0.5" +resolve@^1.3.3: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -6322,6 +6349,10 @@ shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" +shortid@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" diff --git a/src/defaultProps.js b/src/defaultProps.js index 46f8962..a28e067 100644 --- a/src/defaultProps.js +++ b/src/defaultProps.js @@ -22,6 +22,7 @@ export default { collapseOnDataChange: true, freezeWhenExpanded: false, sortable: true, + multiSort: true, resizable: true, filterable: false, defaultSortDesc: false, @@ -29,12 +30,14 @@ export default { defaultFiltered: [], defaultResized: [], defaultExpanded: {}, + // eslint-disable-next-line no-unused-vars defaultFilterMethod: (filter, row, column) => { const id = filter.pivotId || filter.id return row[id] !== undefined ? String(row[id]).startsWith(filter.value) : true }, + // eslint-disable-next-line no-unused-vars defaultSortMethod: (a, b, desc) => { // force null and undefined to the bottom a = a === null || a === undefined ? '' : a @@ -49,7 +52,8 @@ export default { if (a < b) { return -1 } - // returning 0, undefined or any falsey value will use subsequent sorts or the index as a tiebreaker + // returning 0, undefined or any falsey value will use subsequent sorts or + // the index as a tiebreaker return 0 }, @@ -176,24 +180,24 @@ export default { TbodyComponent: _.makeTemplateComponent('rt-tbody', 'Tbody'), TrGroupComponent: _.makeTemplateComponent('rt-tr-group', 'TrGroup'), TrComponent: _.makeTemplateComponent('rt-tr', 'Tr'), - ThComponent: ({ toggleSort, className, children, ...rest }) => { - return ( -
{ - toggleSort && toggleSort(e) - }} - {...rest} - > - {children} -
- ) - }, + ThComponent: ({ toggleSort, className, children, ...rest }) => ( +
( + toggleSort && toggleSort(e) + )} + role="button" + tabIndex={0} + {...rest} + > + {children} +
+ ), TdComponent: _.makeTemplateComponent('rt-td', 'Td'), TfootComponent: _.makeTemplateComponent('rt-tfoot', 'Tfoot'), FilterComponent: ({ filter, onChange }) => ( typeof d[column.id] !== 'undefined') .map((row, i) => ( + // eslint-disable-next-line react/no-array-index-key {row[column.id]} {i < subRows.length - 1 ? ', ' : ''} @@ -236,7 +241,7 @@ export default { className={classnames('-loading', { '-active': loading }, className)} {...rest} > -
+
{loadingText}
diff --git a/src/hoc/README.md b/src/hoc/README.md new file mode 100644 index 0000000..69711c0 --- /dev/null +++ b/src/hoc/README.md @@ -0,0 +1,122 @@ + +
+ React Table Logo +
+ +# ReactTable - expanding with HOCs +This documentation is about expanding ReactTable using Higher Order Components/Functions. + +## Covered in this README +- A Brief explanation of HOCs and why they are a good approach for ReactTable enhancements +- Documentation of the currently available HOCs + - TreeTable + - SelectTable +- Documentation of the standard for writing HOCs with ReactTable + +## What are HOCs and why use them with ReactTable +HOCs (or Higher Order Components/Functions) are either a React Component (or a function that returns a React Component) +that are used to enhance the functionality of an existing component. How much you can enhance depends on the props that +the component exposes. + +Fortunately, ReactTable exposes a LOT of functionality as props to the component. In some cases there are too many +props to keep track of and that is where HOCs come in. + +You can write a HOC that just focusses on the additional functionality you want to enhance and keep those enhancements to +reuse over and over again when you need them. You don't have to edit the ReactSource code, just wrap ReactTable in one or +more HOCs (more on some issues related to chaining HOCs later) that provide the additional functionality you want to expose. + +The most obvious HOC is one that can add `checkbox` or select functionality. The HOC included provides `select` functionality +that allows the developer to specify if they want a `checkbox` or `radio` style of select column. The implementation of the +selection is recorded (e.g. in component state, Redux, etc.) and how to manage multiple selections. The HOC really only handles +the rendering pieces. + +But there is more documentation on the `select` HOC below. + + +## Currently Available HOCs + +### TreeTable +TreeTable takes over the rendering of the generated pivot rows of ReactTable so that they appear more like an expandable Tree. + +It accomplishes this by rendering a 100% wide div and then only rendering the cell that controls the pivot at that level. + +Using it is as simple as doing the following: +```javascript +import ReactTable from 'react-table' +import treeTableHOC from 'react-table/lib/hoc/treeTable' + +const TreeTable = treeTableHOC(ReactTable) +``` +After you have done the above, you can then use `TreeTable` just as you would `ReactTable` but it will render pivots using +the Tree style described above. + + +### SelectTable +SelectTable is a little trickier. The HOCs attempt to avoid adding additional state and, as there is no internal ID for a row that +can be relied on to be static (ReactTable just reuses indexes when rendering) the developer has to maintain the state outside of even +the wrapped component. So it is largely based on callbacks. + +You include the HOC in the same manner as you would for the treeTableHOC but then need to provide the following overrides: +- isSelected - returns `true` if the key passed is selected otherwise it should return `false` +- selectAll - a property that indicates if the selectAll is set (`true|false`) +- toggleAll - called when the user clicks the `selectAll` checkbox/radio +- toggleSelection - called when the use clicks a specific checkbox/radio in a row +- selectType - either `checkbox|radio` to indicate what type of selection is required + +In the case of `radio` there is no `selectAll` displayed but the developer is responsible for only making one selection in +the controlling component's state. You could select multiple but it wouldn't make sense and you should use `checkbox` instead. + +You also have to decide what `selectAll` means. Given ReactTable is a paged solution there are other records off-page. When someone +selects the `selectAll` checkbox, should it mark every possible record, only what might be visible to due a Filter or only those items +on the current page? + +The example opts for the middle approach so it gets a `ref` to the ReactTable instance and pulls the `sortedData` out of the resolved +state (then walks through those records and pulls their ID into the `selection` state of the controlling component). + +You can also replace the input component that is used to render the select box and select all box: +- SelectAllInputComponent - the checkbox in the top left corner +- SelectInputComponent - the checkbox used on a row + +### SelectTreeTable +SelectTreeTable is a combination of TreeTable and SelectTable. + +To function correctly the chain has to be in the correct order as follows (see the comments in the guid on HOCs below). + +```Javascript +const SelectTreeTable = selectTableHOC(treeTableHOC(ReactTable)); +``` + +In this particular instance it is (probably) because the functions need access to the state on the wrapped component to manage +the selected items. Although that is not totally clearly the issue. + +## HOC Guide for ReactTable +There are a few rules required when writing a HOC for ReactTable (other than meeting the normal lint standards - which are +still being developed). + +Firstly, there are issues with `ref` when you write a HOC. Consider a deeply nested component wrapped in multiple HOCs... + +A HOC in the middle of the chain requires access to the instance of the component it thinks it is wrapping but there is at +least one other wrapper in the way. The challenge is: How do I get to the actual wrapped component? + +Each HOC is required to be a React Class so that a `ref` can be obtained against each component: + +```Javascript + this.wrappedInstance = r} /> +``` +*NOTE:* "Component" can also be the `` instance. + +Then the following method needs +to be placed on the class so that it exposes the correct instance of ReactTable: + +```Javascript +getWrappedInstance() { + if (!this.wrappedInstance) console.warn(' - No wrapped instance') + if (this.wrappedInstance.getWrappedInstance) return this.wrappedInstance.getWrappedInstance() + else return this.wrappedInstance +} +``` +Essentially this will walk down the chain (if there are chained HOCs) and stop when it gets to the end and return the wrapped instance. + +Finally, sometimes the chains need to be in a specific order to function correctly. It is not clear if this is just an architectural +issue or if it would be better solved using a library like `recompose`. Anyone who is able to contribute a reliable solution to this +is welcome to submit a PR. diff --git a/src/hoc/selectTable/index.js b/src/hoc/selectTable/index.js new file mode 100644 index 0000000..27ca3b5 --- /dev/null +++ b/src/hoc/selectTable/index.js @@ -0,0 +1,111 @@ +/* eslint-disable */ + +import React from 'react'; + +const defaultSelectInputComponent = (props) => { + return ( + { + const { shiftKey } = e; + e.stopPropagation(); + props.onClick(props.id, shiftKey, props.row); + }} + onChange={()=>{}} + /> + ) +} + +export default (Component) => { + + const wrapper = class RTSelectTable extends React.Component { + + constructor(props) + { + super(props); + } + + rowSelector(row) { + if(!row || !row.hasOwnProperty(this.props.keyField)) return null; + const { toggleSelection, selectType, keyField } = this.props; + const checked = this.props.isSelected(row[this.props.keyField]); + const inputProps = + { + checked, + onClick: toggleSelection, + selectType, + id: row[keyField], + row, + } + return React.createElement(this.props.SelectInputComponent,inputProps); + } + + headSelector(row) { + const { selectType } = this.props; + if (selectType === 'radio') return null; + + const { toggleAll, selectAll: checked, SelectAllInputComponent, } = this.props; + const inputProps = + { + checked, + onClick: toggleAll, + selectType, + } + + return React.createElement(SelectAllInputComponent,inputProps); + } + + // this is so we can expose the underlying ReactTable to get at the sortedData for selectAll + getWrappedInstance() { + if (!this.wrappedInstance) console.warn('RTSelectTable - No wrapped instance'); + if (this.wrappedInstance.getWrappedInstance) return this.wrappedInstance.getWrappedInstance(); + else return this.wrappedInstance + } + + render() + { + const { + columns:originalCols, isSelected, toggleSelection, toggleAll, keyField, selectAll, + selectType, SelectAllInputComponent, SelectInputComponent, + ...rest + } = this.props; + const select = { + id: '_selector', + accessor: ()=>'x', // this value is not important + Header: this.headSelector.bind(this), + Cell: (ci) => { return this.rowSelector.bind(this)(ci.original); }, + width: 30, + filterable: false, + sortable: false, + resizable: false, + style: { textAlign: 'center' }, + } + const columns = [ + select, + ...originalCols, + ]; + const extra = { + columns, + }; + return ( + this.wrappedInstance=r}/> + ) + } + } + + wrapper.displayName = 'RTSelectTable'; + wrapper.defaultProps = + { + keyField: '_id', + isSelected: (key)=>{ console.log('No isSelected handler provided:',{key})}, + selectAll: false, + toggleSelection: (key, shift, row)=>{ console.log('No toggleSelection handler provided:', { key, shift, row }) }, + toggleAll: () => { console.log('No toggleAll handler provided.') }, + selectType: 'check', + SelectInputComponent: defaultSelectInputComponent, + SelectAllInputComponent: defaultSelectInputComponent, + } + + return wrapper; +} diff --git a/src/hoc/treeTable/index.js b/src/hoc/treeTable/index.js new file mode 100644 index 0000000..6c9fa7d --- /dev/null +++ b/src/hoc/treeTable/index.js @@ -0,0 +1,80 @@ +/* eslint-disable */ + +import React from 'react' + +export default (Component) => { + const wrapper = class RTTreeTable extends React.Component { + + constructor(props) + { + super(props); + this.getWrappedInstance.bind(this); + this.TrComponent.bind(this); + this.getTrProps.bind(this); + } + + // this is so we can expose the underlying ReactTable to get at the sortedData for selectAll + getWrappedInstance = () => { + if (!this.wrappedInstance) console.warn('RTTreeTable - No wrapped instance'); + if (this.wrappedInstance.getWrappedInstance) return this.wrappedInstance.getWrappedInstance(); + else return this.wrappedInstance + } + + TrComponent = (props) => { + const { + ri, + ...rest + } = props; + if(ri && ri.groupedByPivot) { + const cell = {...props.children[ri.level]}; + + cell.props.style.flex = 'unset'; + cell.props.style.width = '100%'; + cell.props.style.maxWidth = 'unset'; + cell.props.style.paddingLeft = `${this.props.treeTableIndent*ri.level}px`; + // cell.props.style.backgroundColor = '#DDD'; + cell.props.style.borderBottom = '1px solid rgba(128,128,128,0.2)'; + + return
{cell}
; + } + return ; + } + + getTrProps = (state,ri,ci,instance) => { + return {ri}; + } + + render() { + const { columns, treeTableIndent, ...rest } = this.props; + const { TrComponent, getTrProps } = this; + const extra = { + columns: columns.map((col)=>{ + let column = col; + if(rest.pivotBy && rest.pivotBy.includes(col.accessor)) + { + column = { + accessor: col.accessor, + width: `${treeTableIndent}px`, + show: false, + Header: '', + } + } + return column; + }), + TrComponent, + getTrProps, + }; + + return ( + this.wrappedInstance=r }/> + ) + } + } + wrapper.displayName = 'RTTreeTable'; + wrapper.defaultProps = + { + treeTableIndent: 10, + } + + return wrapper; +} diff --git a/src/index.js b/src/index.js index b3d26f1..12d3eed 100644 --- a/src/index.js +++ b/src/index.js @@ -80,6 +80,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { loadingText, noDataText, sortable, + multiSort, resizable, filterable, // Pivoting State @@ -141,27 +142,25 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const hasColumnFooter = allVisibleColumns.some(d => d.Footer) const hasFilters = filterable || allVisibleColumns.some(d => d.filterable) - const recurseRowsViewIndex = (rows, path = [], index = -1) => { - return [ - rows.map((row, i) => { - index++ - const rowWithViewIndex = { - ...row, - _viewIndex: index, - } - const newPath = path.concat([i]) - if (rowWithViewIndex[subRowsKey] && _.get(expanded, newPath)) { - ;[rowWithViewIndex[subRowsKey], index] = recurseRowsViewIndex( - rowWithViewIndex[subRowsKey], - newPath, - index - ) - } - return rowWithViewIndex - }), - index, - ] - } + const recurseRowsViewIndex = (rows, path = [], index = -1) => ([ + rows.map((row, i) => { + index += 1 + const rowWithViewIndex = { + ...row, + _viewIndex: index, + } + const newPath = path.concat([i]) + if (rowWithViewIndex[subRowsKey] && _.get(expanded, newPath)) { + [rowWithViewIndex[subRowsKey], index] = recurseRowsViewIndex( + rowWithViewIndex[subRowsKey], + newPath, + index, + ) + } + return rowWithViewIndex + }), + index, + ]) ;[pageRows] = recurseRowsViewIndex(pageRows) const canPrevious = page > 0 @@ -171,7 +170,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { allVisibleColumns.map(d => { const resizedColumn = resized.find(x => x.id === d.id) || {} return _.getFirstDefined(resizedColumn.value, d.width, d.minWidth) - }) + }), ) let rowIndex = -1 @@ -189,59 +188,45 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { rowMinWidth, } - // Visual Components + const rootProps = _.splitProps( + getProps(finalState, undefined, undefined, this), + ) + const tableProps = _.splitProps( + getTableProps(finalState, undefined, undefined, this), + ) + const tBodyProps = _.splitProps( + getTbodyProps(finalState, undefined, undefined, this), + ) + const loadingProps = getLoadingProps(finalState, undefined, undefined, this) + const noDataProps = getNoDataProps(finalState, undefined, undefined, this) + const resizerProps = getResizerProps(finalState, undefined, undefined, this) - const makeHeaderGroups = () => { - const theadGroupProps = _.splitProps( - getTheadGroupProps(finalState, undefined, undefined, this) - ) - const theadGroupTrProps = _.splitProps( - getTheadGroupTrProps(finalState, undefined, undefined, this) - ) - return ( - - - {headerGroups.map(makeHeaderGroup)} - - - ) - } + // Visual Components const makeHeaderGroup = (column, i) => { const resizedValue = col => (resized.find(x => x.id === col.id) || {}).value const flex = _.sum( column.columns.map( - col => (col.width || resizedValue(col) ? 0 : col.minWidth) - ) + col => (col.width || resizedValue(col) ? 0 : col.minWidth), + ), ) const width = _.sum( column.columns.map(col => - _.getFirstDefined(resizedValue(col), col.width, col.minWidth) - ) + _.getFirstDefined(resizedValue(col), col.width, col.minWidth), + ), ) const maxWidth = _.sum( column.columns.map(col => - _.getFirstDefined(resizedValue(col), col.width, col.maxWidth) - ) + _.getFirstDefined(resizedValue(col), col.width, col.maxWidth), + ), ) const theadGroupThProps = _.splitProps( - getTheadGroupThProps(finalState, undefined, column, this) + getTheadGroupThProps(finalState, undefined, column, this), ) const columnHeaderProps = _.splitProps( - column.getHeaderProps(finalState, undefined, column, this) + column.getHeaderProps(finalState, undefined, column, this), ) const classes = [ @@ -269,7 +254,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { return ( {_.normalizeComponent(column.Header, { data: sortedData, - column: column, + column, })} ) } - const makeHeaders = () => { - const theadProps = _.splitProps( - getTheadProps(finalState, undefined, undefined, this) + const makeHeaderGroups = () => { + const theadGroupProps = _.splitProps( + getTheadGroupProps(finalState, undefined, undefined, this), ) - const theadTrProps = _.splitProps( - getTheadTrProps(finalState, undefined, undefined, this) + const theadGroupTrProps = _.splitProps( + getTheadGroupTrProps(finalState, undefined, undefined, this), ) return ( - {allVisibleColumns.map(makeHeader)} + {headerGroups.map(makeHeaderGroup)} ) @@ -320,18 +305,18 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const width = _.getFirstDefined( resizedCol.value, column.width, - column.minWidth + column.minWidth, ) const maxWidth = _.getFirstDefined( resizedCol.value, column.width, - column.maxWidth + column.maxWidth, ) const theadThProps = _.splitProps( - getTheadThProps(finalState, undefined, column, this) + getTheadThProps(finalState, undefined, column, this), ) const columnHeaderProps = _.splitProps( - column.getHeaderProps(finalState, undefined, column, this) + column.getHeaderProps(finalState, undefined, column, this), ) const classes = [ @@ -364,7 +349,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { return ( { - isSortable && this.sortColumn(column, e.shiftKey) + if (isSortable) this.sortColumn(column, multiSort ? e.shiftKey : false) }} {...rest} >
{_.normalizeComponent(column.Header, { data: sortedData, - column: column, + column, })}
{resizer} @@ -397,28 +382,28 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) } - const makeFilters = () => { - const theadFilterProps = _.splitProps( - getTheadFilterProps(finalState, undefined, undefined, this) + const makeHeaders = () => { + const theadProps = _.splitProps( + getTheadProps(finalState, undefined, undefined, this), ) - const theadFilterTrProps = _.splitProps( - getTheadFilterTrProps(finalState, undefined, undefined, this) + const theadTrProps = _.splitProps( + getTheadTrProps(finalState, undefined, undefined, this), ) return ( - {allVisibleColumns.map(makeFilter)} + {allVisibleColumns.map(makeHeader)} ) @@ -429,18 +414,18 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const width = _.getFirstDefined( resizedCol.value, column.width, - column.minWidth + column.minWidth, ) const maxWidth = _.getFirstDefined( resizedCol.value, column.width, - column.maxWidth + column.maxWidth, ) const theadFilterThProps = _.splitProps( - getTheadFilterThProps(finalState, undefined, column, this) + getTheadFilterThProps(finalState, undefined, column, this), ) const columnHeaderProps = _.splitProps( - column.getHeaderProps(finalState, undefined, column, this) + column.getHeaderProps(finalState, undefined, column, this), ) const classes = [ @@ -467,12 +452,12 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const isFilterable = _.getFirstDefined( column.filterable, filterable, - false + false, ) return ( this.filterColumn(column, value), }, - defaultProps.column.Filter + defaultProps.column.Filter, ) : null} ) } + const makeFilters = () => { + const theadFilterProps = _.splitProps( + getTheadFilterProps(finalState, undefined, undefined, this), + ) + const theadFilterTrProps = _.splitProps( + getTheadFilterTrProps(finalState, undefined, undefined, this), + ) + return ( + + + {allVisibleColumns.map(makeFilter)} + + + ) + } + const makePageRow = (row, i, path = []) => { const rowInfo = { original: row[originalKey], - row: row, + row, index: row[indexKey], - viewIndex: ++rowIndex, - pageSize: pageSize, - page: page, + viewIndex: rowIndex += 1, + pageSize, + page, level: path.length, nestingPath: path.concat([i]), aggregated: row[aggregatedKey], @@ -514,14 +526,14 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { const isExpanded = _.get(expanded, rowInfo.nestingPath) const trGroupProps = getTrGroupProps(finalState, rowInfo, undefined, this) const trProps = _.splitProps( - getTrProps(finalState, rowInfo, undefined, this) + getTrProps(finalState, rowInfo, undefined, this), ) return ( { + () => ( onExpandedChange && onExpandedChange(newExpanded, cellInfo.nestingPath, e) - } + ), ) } @@ -605,7 +617,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { let resolvedCell = _.normalizeComponent( column.Cell, cellInfo, - value + value, ) // Resolve Renderers @@ -632,7 +644,8 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { // Make it expandable by defualt cellInfo.expandable = true useOnExpanderClick = true - // If pivoted, has no subRows, and does not have a subComponent, do not make expandable + // If pivoted, has no subRows, and does not have a subComponent, + // do not make expandable if (cellInfo.pivoted && !cellInfo.subRows && !SubComponent) { cellInfo.expandable = false } @@ -655,14 +668,14 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ...cellInfo, value: row[pivotValKey], }, - row[pivotValKey] + row[pivotValKey], ) } else if (isPreview) { // Show the pivot preview resolvedCell = _.normalizeComponent( ResolvedAggregatedComponent, cellInfo, - value + value, ) } else { resolvedCell = null @@ -671,7 +684,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { resolvedCell = _.normalizeComponent( ResolvedAggregatedComponent, cellInfo, - value + value, ) } @@ -679,7 +692,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { resolvedCell = _.normalizeComponent( ResolvedExpanderComponent, cellInfo, - row[pivotValKey] + row[pivotValKey], ) if (pivotBy) { if (cellInfo.groupedByPivot) { @@ -695,7 +708,9 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ? onExpanderClick : () => {} - // If there are multiple onClick events, make sure they don't override eachother. This should maybe be expanded to handle all function attributes + // If there are multiple onClick events, make sure they don't + // override eachother. This should maybe be expanded to handle all + // function attributes const interactionProps = { onClick: resolvedOnExpanderClick, } @@ -715,12 +730,13 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { // Return the cell return ( - makePageRow(d, i, rowInfo.nestingPath) + makePageRow(d, i, rowInfo.nestingPath), )} {SubComponent && !rowInfo.subRows && @@ -750,52 +766,26 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { ) } - const makePadRow = (row, i) => { - const trGroupProps = getTrGroupProps( - finalState, - undefined, - undefined, - this - ) - const trProps = _.splitProps( - getTrProps(finalState, undefined, undefined, this) - ) - return ( - - - {allVisibleColumns.map(makePadColumn)} - - - ) - } - const makePadColumn = (column, i) => { const resizedCol = resized.find(x => x.id === column.id) || {} const show = typeof column.show === 'function' ? column.show() : column.show - let width = _.getFirstDefined( + const width = _.getFirstDefined( resizedCol.value, column.width, - column.minWidth + column.minWidth, ) - let flex = width - let maxWidth = _.getFirstDefined( + const flex = width + const maxWidth = _.getFirstDefined( resizedCol.value, column.width, - column.maxWidth + column.maxWidth, ) const tdProps = _.splitProps( - getTdProps(finalState, undefined, column, this) + getTdProps(finalState, undefined, column, this), ) const columnProps = _.splitProps( - column.getProps(finalState, undefined, column, this) + column.getProps(finalState, undefined, column, this), ) const classes = [ @@ -812,7 +802,7 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { return ( { + const trGroupProps = getTrGroupProps( + finalState, + undefined, + undefined, + this, + ) + const trProps = _.splitProps( + getTrProps(finalState, undefined, undefined, this), + ) + return ( + + + {allVisibleColumns.map(makePadColumn)} + + + ) + } + + const makeColumnFooter = (column, i) => { + const resizedCol = resized.find(x => x.id === column.id) || {} + const show = + typeof column.show === 'function' ? column.show() : column.show + const width = _.getFirstDefined( + resizedCol.value, + column.width, + column.minWidth, + ) + const maxWidth = _.getFirstDefined( + resizedCol.value, + column.width, + column.maxWidth, + ) + const tFootTdProps = _.splitProps( + getTfootTdProps(finalState, undefined, undefined, this), + ) + const columnProps = _.splitProps( + column.getProps(finalState, undefined, column, this), + ) + const columnFooterProps = _.splitProps( + column.getFooterProps(finalState, undefined, column, this), + ) + + const classes = [ + tFootTdProps.className, + column.className, + columnProps.className, + columnFooterProps.className, + ] + + const styles = { + ...tFootTdProps.style, + ...column.style, + ...columnProps.style, + ...columnFooterProps.style, + } + + return ( + + {_.normalizeComponent(column.Footer, { + data: sortedData, + column, + })} + + ) + } + const makeColumnFooters = () => { const tFootProps = getTfootProps(finalState, undefined, undefined, this) const tFootTrProps = _.splitProps( - getTfootTrProps(finalState, undefined, undefined, this) + getTfootTrProps(finalState, undefined, undefined, this), ) return ( { - const resizedCol = resized.find(x => x.id === column.id) || {} - const show = - typeof column.show === 'function' ? column.show() : column.show - const width = _.getFirstDefined( - resizedCol.value, - column.width, - column.minWidth - ) - const maxWidth = _.getFirstDefined( - resizedCol.value, - column.width, - column.maxWidth - ) - const tFootTdProps = _.splitProps( - getTfootTdProps(finalState, undefined, undefined, this) - ) - const columnProps = _.splitProps( - column.getProps(finalState, undefined, column, this) - ) - const columnFooterProps = _.splitProps( - column.getFooterProps(finalState, undefined, column, this) - ) - - const classes = [ - tFootTdProps.className, - column.className, - columnProps.className, - columnFooterProps.className, - ] - - const styles = { - ...tFootTdProps.style, - ...column.style, - ...columnProps.style, - ...columnFooterProps.style, - } - - return ( - - {_.normalizeComponent(column.Footer, { - data: sortedData, - column: column, - })} - - ) - } - const makePagination = () => { const paginationProps = _.splitProps( - getPaginationProps(finalState, undefined, undefined, this) + getPaginationProps(finalState, undefined, undefined, this), ) return ( { const pagination = makePagination() return ( @@ -956,14 +959,14 @@ export default class ReactTable extends Methods(Lifecycle(Component)) { {...rootProps.rest} > {showPagination && showPaginationTop - ?
+ ?
{pagination}
: null} {showPagination && showPaginationBottom - ?
+ ?
{pagination}
: null} diff --git a/src/lifecycle.js b/src/lifecycle.js index 7020736..18a16e0 100644 --- a/src/lifecycle.js +++ b/src/lifecycle.js @@ -64,7 +64,7 @@ export default Base => if (freezeWhenExpanded) { // if any rows are expanded, freeze the existing data and sorting const keys = Object.keys(newResolvedState.expanded) - for (var i = 0; i < keys.length; i++) { + for (let i = 0; i < keys.length; i += 1) { if (newResolvedState.expanded[keys[i]]) { newResolvedState.frozen = true break @@ -109,18 +109,18 @@ export default Base => newResolvedState.pages = newResolvedState.manual ? newResolvedState.pages : Math.ceil( - newResolvedState.sortedData.length / newResolvedState.pageSize + newResolvedState.sortedData.length / newResolvedState.pageSize, ) newResolvedState.page = Math.max( newResolvedState.page >= newResolvedState.pages ? newResolvedState.pages - 1 : newResolvedState.page, - 0 + 0, ) } return this.setState(newResolvedState, () => { - cb && cb() + if (cb) { cb() } if ( oldState.page !== newResolvedState.page || oldState.pageSize !== newResolvedState.pageSize || diff --git a/src/methods.js b/src/methods.js index c6a29bf..c473ec1 100644 --- a/src/methods.js +++ b/src/methods.js @@ -42,7 +42,7 @@ export default Base => let expanderColumn = columns.find( col => col.expander || - (col.columns && col.columns.some(col2 => col2.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) { @@ -91,48 +91,48 @@ export default Base => 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.' + 'A column id is required if using a non-string accessor for column above.', ) } // Fall back to an undefined accessor if (!dcol.accessor) { - dcol.accessor = d => undefined + dcol.accessor = () => undefined } return dcol } + const allDecoratedColumns = [] + // Decorate the columns const decorateAndAddToAll = (column, parentColumn) => { const decoratedColumn = makeDecoratedColumn(column, parentColumn) allDecoratedColumns.push(decoratedColumn) return decoratedColumn } - const allDecoratedColumns = [] - const decoratedColumns = columnsWithExpander.map((column, i) => { + + const decoratedColumns = columnsWithExpander.map(column => { if (column.columns) { return { ...column, columns: column.columns.map(d => decorateAndAddToAll(d, column)), } - } else { - return decorateAndAddToAll(column) } + return decorateAndAddToAll(column) }) // Build the visible columns, headers and flat column list let visibleColumns = decoratedColumns.slice() let allVisibleColumns = [] - visibleColumns = visibleColumns.map((column, i) => { + visibleColumns = visibleColumns.map(column => { if (column.columns) { - const visibleSubColumns = column.columns.filter( - d => - pivotBy.indexOf(d.id) > -1 - ? false - : _.getFirstDefined(d.show, true) - ) + const visibleSubColumns = column.columns.filter(d => ( + pivotBy.indexOf(d.id) > -1 + ? false + : _.getFirstDefined(d.show, true) + )) return { ...column, columns: visibleSubColumns, @@ -141,13 +141,13 @@ export default Base => return column }) - visibleColumns = visibleColumns.filter(column => { - return column.columns + visibleColumns = visibleColumns.filter(column => ( + column.columns ? column.columns.length : pivotBy.indexOf(column.id) > -1 ? false : _.getFirstDefined(column.show, true) - }) + )) // Find any custom pivot location const pivotIndex = visibleColumns.findIndex(col => col.pivot) @@ -163,10 +163,10 @@ export default Base => } }) - let PivotParentColumn = pivotColumns.reduce( + const PivotParentColumn = pivotColumns.reduce( (prev, current) => prev && prev === current.parentColumn && current.parentColumn, - pivotColumns[0].parentColumn + pivotColumns[0].parentColumn, ) let PivotGroupHeader = hasHeaderGroups && PivotParentColumn.Header @@ -202,13 +202,13 @@ export default Base => headerGroups.push({ ...this.props.column, ...column, - columns: columns, + columns, }) currentSpan = [] } // Build flast list of allVisibleColumns and HeaderGroups - visibleColumns.forEach((column, i) => { + visibleColumns.forEach(column => { if (column.columns) { allVisibleColumns = allVisibleColumns.concat(column.columns) if (currentSpan.length > 0) { @@ -238,13 +238,18 @@ export default Base => }) if (row[subRowsKey]) { row[subRowsKey] = row[subRowsKey].map((d, i) => - accessRow(d, i, level + 1) + accessRow(d, i, level + 1), ) } return row } let resolvedData = data.map((d, i) => accessRow(d, i)) + // TODO: Make it possible to fabricate nested rows without pivoting + const aggregatingColumns = allVisibleColumns.filter( + d => !d.expander && d.aggregate, + ) + // If pivoting, recursively group the data const aggregate = rows => { const aggregationValues = {} @@ -254,11 +259,6 @@ export default Base => }) return aggregationValues } - - // TODO: Make it possible to fabricate nested rows without pivoting - const aggregatingColumns = allVisibleColumns.filter( - d => !d.expander && d.aggregate - ) if (pivotBy.length) { const groupRecursively = (rows, keys, i = 0) => { // This is the last level, just return the rows @@ -267,20 +267,18 @@ export default Base => } // 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, - [nestingLevelKey]: i, - [groupedByPivotKey]: true, - } - }) + _.groupBy(rows, keys[i]), + ).map(([key, value]) => ({ + [pivotIDKey]: keys[i], + [pivotValKey]: key, + [keys[i]]: key, + [subRowsKey]: value, + [nestingLevelKey]: i, + [groupedByPivotKey]: true, + })) // Recurse into the subRows groupedRows = groupedRows.map(rowGroup => { - let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1) + const subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1) return { ...rowGroup, [subRowsKey]: subRows, @@ -329,10 +327,10 @@ export default Base => resolvedData, filtered, defaultFilterMethod, - allVisibleColumns + allVisibleColumns, ), sorted, - sortMethodsByColumnID + sortMethodsByColumnID, ), } } @@ -366,11 +364,10 @@ export default Base => // If 'filterAll' is set to true, pass the entire dataset to the filter method if (column.filterAll) { return filterMethod(nextFilter, filteredSoFar, column) - } else { - return filteredSoFar.filter(row => { - return filterMethod(nextFilter, row, column) - }) } + return filteredSoFar.filter(row => ( + filterMethod(nextFilter, row, column) + )) }, filteredData) // Apply the filter to the subrows if we are pivoting, and then @@ -386,7 +383,7 @@ export default Base => row[this.props.subRowsKey], filtered, defaultFilterMethod, - allVisibleColumns + allVisibleColumns, ), } }) @@ -411,16 +408,16 @@ export default Base => sorted.map(sort => { // Support custom sorting methods for each column if (sortMethodsByColumnID[sort.id]) { - return (a, b) => { - return sortMethodsByColumnID[sort.id](a[sort.id], b[sort.id], sort.desc) - } - } - return (a, b) => { - return this.props.defaultSortMethod(a[sort.id], b[sort.id], sort.desc) + return (a, b) => ( + sortMethodsByColumnID[sort.id](a[sort.id], b[sort.id], sort.desc) + ) } + return (a, b) => ( + this.props.defaultSortMethod(a[sort.id], b[sort.id], sort.desc) + ) }), sorted.map(d => !d.desc), - this.props.indexKey + this.props.indexKey, ) sortedData.forEach(row => { @@ -430,7 +427,7 @@ export default Base => row[this.props.subRowsKey] = this.sortData( row[this.props.subRowsKey], sorted, - sortMethodsByColumnID + sortMethodsByColumnID, ) }) @@ -440,7 +437,7 @@ export default Base => getMinRows () { return _.getFirstDefined( this.props.minRows, - this.getStateOrProp('pageSize') + this.getStateOrProp('pageSize'), ) } @@ -452,9 +449,9 @@ export default Base => if (collapseOnPageChange) { newState.expanded = {} } - this.setStateWithData(newState, () => { + this.setStateWithData(newState, () => ( onPageChange && onPageChange(page) - }) + )) } onPageSizeChange (newPageSize) { @@ -470,16 +467,16 @@ export default Base => pageSize: newPageSize, page: newPage, }, - () => { + () => ( onPageSizeChange && onPageSizeChange(newPageSize, newPage) - } + ), ) } sortColumn (column, additive) { const { sorted, skipNextSort, defaultSortDesc } = this.getResolvedState() - const firstSortDirection = column.hasOwnProperty('defaultSortDesc') + const firstSortDirection = Object.prototype.hasOwnProperty.call(column, 'defaultSortDesc') ? column.defaultSortDesc : defaultSortDesc const secondSortDirection = !firstSortDirection @@ -519,20 +516,18 @@ export default Base => newSorted = [existing] } } + } else if (additive) { + newSorted.push({ + id: column.id, + desc: firstSortDirection, + }) } else { - if (additive) { - newSorted.push({ + newSorted = [ + { id: column.id, desc: firstSortDirection, - }) - } else { - newSorted = [ - { - id: column.id, - desc: firstSortDirection, - }, - ] - } + }, + ] } } else { // Multi-Sort @@ -556,21 +551,19 @@ export default Base => if (!additive) { newSorted = newSorted.slice(existingIndex, column.length) } - } else { - // New Sort Column - if (additive) { - newSorted = newSorted.concat( - column.map(d => ({ - id: d.id, - desc: firstSortDirection, - })) - ) - } else { - newSorted = column.map(d => ({ + // New Sort Column + } else if (additive) { + newSorted = newSorted.concat( + column.map(d => ({ id: d.id, desc: firstSortDirection, - })) - } + })), + ) + } else { + newSorted = column.map(d => ({ + id: d.id, + desc: firstSortDirection, + })) } } @@ -582,9 +575,9 @@ export default Base => : this.state.page, sorted: newSorted, }, - () => { + () => ( onSortedChange && onSortedChange(newSorted, column, additive) - } + ), ) } @@ -593,16 +586,14 @@ export default Base => const { onFilteredChange } = this.props // Remove old filter first if it exists - const newFiltering = (filtered || []).filter(x => { - if (x.id !== column.id) { - return true - } - }) + const newFiltering = (filtered || []).filter(x => ( + x.id !== column.id + )) if (value !== '') { newFiltering.push({ id: column.id, - value: value, + value, }) } @@ -610,9 +601,9 @@ export default Base => { filtered: newFiltering, }, - () => { + () => ( onFilteredChange && onFilteredChange(newFiltering, column, value) - } + ), ) } @@ -634,7 +625,7 @@ export default Base => currentlyResizing: { id: column.id, startX: pageX, - parentWidth: parentWidth, + parentWidth, }, }, () => { @@ -647,7 +638,7 @@ export default Base => document.addEventListener('mouseup', this.resizeColumnEnd) document.addEventListener('mouseleave', this.resizeColumnEnd) } - } + }, ) } @@ -667,10 +658,11 @@ export default Base => pageX = event.pageX } - // Set the min size to 10 to account for margin and border or else the group headers don't line up correctly + // Set the min size to 10 to account for margin and border or else the + // group headers don't line up correctly const newWidth = Math.max( currentlyResizing.parentWidth + pageX - currentlyResizing.startX, - 11 + 11, ) newResized.push({ @@ -682,15 +674,15 @@ export default Base => { resized: newResized, }, - () => { + () => ( onResizedChange && onResizedChange(newResized, event) - } + ), ) } resizeColumnEnd (event) { event.stopPropagation() - let isTouch = event.type === 'touchend' || event.type === 'touchcancel' + const isTouch = event.type === 'touchend' || event.type === 'touchcancel' if (isTouch) { document.removeEventListener('touchmove', this.resizeColumnMoving) diff --git a/src/pagination.js b/src/pagination.js index 496e81d..e5af788 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -4,7 +4,7 @@ import classnames from 'classnames' // import _ from './utils' const defaultButton = props => ( - ) @@ -42,7 +42,7 @@ export default class ReactTablePagination extends Component { } applyPage (e) { - e && e.preventDefault() + if (e) { e.preventDefault() } const page = this.state.page this.changePage(page === '' ? this.props.page : page) } @@ -70,9 +70,9 @@ export default class ReactTablePagination extends Component { className={classnames(className, '-pagination')} style={this.props.paginationStyle} > -
+
{ + onClick={() => { if (!canPrevious) return this.changePage(page - 1) }} @@ -81,11 +81,11 @@ export default class ReactTablePagination extends Component { {this.props.previousText}
-
- +
+ {this.props.pageText}{' '} {showPageJump - ?
+ ?
{ @@ -105,31 +105,30 @@ export default class ReactTablePagination extends Component { }} />
- : + : {page + 1} }{' '} {this.props.ofText}{' '} - {pages || 1} + {pages || 1} {showPageSizeOptions && - + }
-
+
{ + onClick={() => { if (!canNext) return this.changePage(page + 1) }} diff --git a/src/propTypes.js b/src/propTypes.js index 227df7e..48b09a8 100644 --- a/src/propTypes.js +++ b/src/propTypes.js @@ -1,4 +1,4 @@ -import PropTypes from "prop-types"; +import PropTypes from 'prop-types' export default { // General @@ -85,37 +85,37 @@ export default { Cell: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), Header: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), Footer: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), Aggregated: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), Pivot: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), PivotValue: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), Expander: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, - PropTypes.func + PropTypes.func, ]), Filter: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), @@ -145,8 +145,8 @@ export default { getFooterProps: PropTypes.object, filterMethod: PropTypes.func, filterAll: PropTypes.bool, - sortMethod: PropTypes.func - }) + sortMethod: PropTypes.func, + }), ), // Global Expander Column Defaults @@ -154,7 +154,7 @@ export default { sortable: PropTypes.bool, resizable: PropTypes.bool, filterable: PropTypes.bool, - width: PropTypes.number + width: PropTypes.number, }), pivotDefaults: PropTypes.object, @@ -181,7 +181,8 @@ export default { ExpanderComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), PivotValueComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), AggregatedComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - PivotComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), // this is a computed default generated using + // this is a computed default generated using + PivotComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), // the ExpanderComponent and PivotValueComponent at run-time in methods.js PaginationComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), PreviousComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), @@ -189,5 +190,5 @@ export default { LoadingComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), NoDataComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), ResizerComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - PadRowComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]) -}; + PadRowComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), +} diff --git a/src/utils.js b/src/utils.js index a99b653..3848f47 100644 --- a/src/utils.js +++ b/src/utils.js @@ -30,7 +30,9 @@ function get (obj, path, def) { let val try { val = pathObj.reduce((current, pathPart) => current[pathPart], obj) - } catch (e) {} + } catch (e) { + // continue regardless of error + } return typeof val !== 'undefined' ? val : def } @@ -59,7 +61,7 @@ function last (arr) { function range (n) { const arr = [] - for (let i = 0; i < n; i++) { + for (let i = 0; i < n; i += 1) { arr.push(n) } return arr @@ -67,7 +69,7 @@ function range (n) { function orderBy (arr, funcs, dirs, indexKey) { return arr.sort((rowA, rowB) => { - for (let i = 0; i < funcs.length; i++) { + for (let i = 0; i < funcs.length; i += 1) { const comp = funcs[i] const desc = dirs[i] === false || dirs[i] === 'desc' const sortInt = comp(rowA, rowB) @@ -83,8 +85,8 @@ function orderBy (arr, funcs, dirs, indexKey) { } function remove (a, b) { - return a.filter(function (o, i) { - var r = b(o) + return a.filter((o, i) => { + const r = b(o) if (r) { a.splice(i, 1) return true @@ -101,7 +103,7 @@ function clone (a) { return value.toString() } return value - }) + }), ) } catch (e) { return a @@ -109,7 +111,7 @@ function clone (a) { } function getFirstDefined (...args) { - for (var i = 0; i < args.length; i++) { + for (let i = 0; i < args.length; i += 1) { if (typeof args[i] !== 'undefined') { return args[i] } @@ -117,9 +119,9 @@ function getFirstDefined (...args) { } function sum (arr) { - return arr.reduce((a, b) => { - return a + b - }, 0) + return arr.reduce((a, b) => ( + a + b + ), 0) } function makeTemplateComponent (compClass, displayName) { @@ -146,7 +148,7 @@ function groupBy (xs, key) { function asPx (value) { value = Number(value) - return Number.isNaN(value) ? null : value + 'px' + return Number.isNaN(value) ? null : `${value}px` } function isArray (a) { @@ -169,7 +171,7 @@ function flattenDeep (arr, newArr = []) { if (!isArray(arr)) { newArr.push(arr) } else { - for (var i = 0; i < arr.length; i++) { + for (let i = 0; i < arr.length; i += 1) { flattenDeep(arr[i], newArr) } } @@ -186,15 +188,16 @@ function splitProps ({ className, style, ...rest }) { function compactObject (obj) { const newObj = {} - for (var key in obj) { + Object.keys(obj).map(key => { if ( - obj.hasOwnProperty(key) && + Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined && typeof obj[key] !== 'undefined' ) { newObj[key] = obj[key] } - } + return true + }) return newObj }