From dd4c1e2df6ba35863cce3cd18aa47e77c66b2d45 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 25 May 2018 06:32:34 +0800 Subject: [PATCH] add FoldableTable (#937) I have used this library to develop a big application for my company and had been required to make the columns are foldable. I saw that feature is useful and would like to share with everybody. Especially to all developers who developed this great component. Let me know if any issues with My component. I will fix it accordingly. --- docs/src/App.js | 14 +- .../foldabletable/FoldableTableCustomState.js | 110 +++++++++ .../foldabletable/FoldableTableWithHeader.js | 71 ++++++ .../FoldableTableWithoutHeader.js | 64 +++++ docs/src/examples/foldabletable/index.js | 30 +++ docs/src/examples/index.js | 14 +- src/hoc/README.md | 101 ++++++++ src/hoc/foldableTable/index.js | 225 ++++++++++++++++++ src/hoc/foldableTable/left.svg | 1 + src/hoc/foldableTable/right.svg | 1 + 10 files changed, 621 insertions(+), 10 deletions(-) create mode 100644 docs/src/examples/foldabletable/FoldableTableCustomState.js create mode 100644 docs/src/examples/foldabletable/FoldableTableWithHeader.js create mode 100644 docs/src/examples/foldabletable/FoldableTableWithoutHeader.js create mode 100644 docs/src/examples/foldabletable/index.js create mode 100644 src/hoc/foldableTable/index.js create mode 100644 src/hoc/foldableTable/left.svg create mode 100644 src/hoc/foldableTable/right.svg diff --git a/docs/src/App.js b/docs/src/App.js index d788f8a..70e68b1 100644 --- a/docs/src/App.js +++ b/docs/src/App.js @@ -15,8 +15,8 @@ import HOCReadme from "./stories/HOCReadme.js"; // import Tester from './examples/expander'; const stories = [ - { name: "Readme", component: Readme }, - { name: "HOC Readme", component: HOCReadme }, + { name: 'Readme', component: Readme }, + { name: 'HOC Readme', component: HOCReadme }, // { name: 'Tester', component: Test }, { name: "Simple Table", component: CodeSandbox("X6npLXPRW") }, @@ -61,10 +61,12 @@ const stories = [ name: "Multiple Pagers (Top and Bottom)", component: CodeSandbox("VEZ8OgvX") }, - { name: "Tree Table (HOC)", component: CodeSandbox("lxmr4wynzq") }, - { name: "Select Table (HOC)", component: CodeSandbox("7yq5ylw09j") }, - { name: "Select Tree Table (HOC)", component: CodeSandbox("2p7jp4klwp") } -]; + + { name: 'Tree Table (HOC)', component: CodeSandbox('lxmr4wynzq') }, + { name: 'Select Table (HOC)', component: CodeSandbox('7yq5ylw09j') }, + { name: 'Select Tree Table (HOC)', component: CodeSandbox('2p7jp4klwp') }, + { name: 'Foldable Table (HOC)', component: CodeSandbox('2p7jp4klwp') }, +] export default class App extends React.Component { render() { diff --git a/docs/src/examples/foldabletable/FoldableTableCustomState.js b/docs/src/examples/foldabletable/FoldableTableCustomState.js new file mode 100644 index 0000000..b2c0f55 --- /dev/null +++ b/docs/src/examples/foldabletable/FoldableTableCustomState.js @@ -0,0 +1,110 @@ +import React from 'react' +import ReactTable from '../../../../lib/index' +import FoldableTableHOC from '../../../../lib/hoc/foldableTable' +import selectTableHOC from '../../../../lib/hoc/selectTable' + +const FoldableTable = FoldableTableHOC(selectTableHOC(ReactTable)) + +export default class FoldableTableCustomState extends React.Component { + constructor(props, context) { + super(props, context) + + this.state = { folded: {}, seleted: {}, selectedAll: false } + } + + getData = () => [{ + id: 1, + first_name: 'Jeanette', + 'last_name': 'Penddreth', + email: 'jpenddreth0@census.gov', + 'gender': 'Female', + ip_address: '26.58.193.2', + }, { + 'id': 2, + 'first_name': 'Giavani', + last_name: 'Frediani', + 'email': 'gfrediani1@senate.gov', + 'gender': 'Male', + 'ip_address': '229.179.4.212', + }, { + 'id': 3, + first_name: 'Noell', + last_name: 'Bea', + 'email': 'nbea2@imageshack.us', + gender: 'Female', + ip_address: '180.66.162.255', + }, { + 'id': 4, + 'first_name': 'Willard', + 'last_name': 'Valek', + email: 'wvalek3@vk.com', + 'gender': 'Male', + ip_address: '67.76.188.26', + }]; + + toggleSelection = (key, shift, row) => { + const { selected } = this.state + let newSelected = Object.assign({}, selected) + newSelected[key] = !newSelected[key] + this.setState(p => ({ selected: newSelected })) + }; + + toggleAll = () => { + const { selectedAll } = this.state + + if (selectedAll) { this.setState(p => { return { selectedAll: false, seleted: {} } }); } + else { + const data = this.getData() + + let newSelected = {} + data.forEach(d => newSelected[d.id] = true) + this.setState(p => ({ selectedAll: true, seleted: newSelected })) + } + } + + isSelected = key => this.state.seleted[key]; + + render() { + return ( this.setState(p => { return { folded: newFolded } })} + folded={this.state.folded} + + data={this.getData()} + columns={[{ + Header: "Name", + foldable: true, + columns: [ + { + Header: "First Name", + accessor: "first_name" + }, + { + Header: "Last Name", + accessor: "last_name" + } + ] + }, { + Header: "Info", + foldable: true, + columns: [ + { + Header: "Email", + accessor: "email" + }, + { + Header: "Gender", + accessor: "gender" + } + ] + }] + }>) + } +} diff --git a/docs/src/examples/foldabletable/FoldableTableWithHeader.js b/docs/src/examples/foldabletable/FoldableTableWithHeader.js new file mode 100644 index 0000000..110f882 --- /dev/null +++ b/docs/src/examples/foldabletable/FoldableTableWithHeader.js @@ -0,0 +1,71 @@ +import React from 'react' +import ReactTable from '../../../../lib/index' +import FoldableTableHOC from '../../../../lib/hoc/foldableTable' + +const FoldableTable = FoldableTableHOC(ReactTable) + +export default class FoldableTableWithHeader extends React.Component { + + getData = () => [{ + id: 1, + first_name: 'Jeanette', + last_name: 'Penddreth', + 'email': 'jpenddreth0@census.gov', + gender: 'Female', + 'ip_address': '26.58.193.2', + }, { + 'id': 2, + first_name: 'Giavani', + last_name: 'Frediani', + email: 'gfrediani1@senate.gov', + gender: 'Male', + 'ip_address': '229.179.4.212', + }, { + 'id': 3, + 'first_name': 'Noell', + last_name: 'Bea', + 'email': 'nbea2@imageshack.us', + 'gender': 'Female', + ip_address: '180.66.162.255', + }, { + 'id': 4, + 'first_name': 'Willard', + 'last_name': 'Valek', + email: 'wvalek3@vk.com', + 'gender': 'Male', + ip_address: '67.76.188.26', + }]; + + render() { + return () + } +} diff --git a/docs/src/examples/foldabletable/FoldableTableWithoutHeader.js b/docs/src/examples/foldabletable/FoldableTableWithoutHeader.js new file mode 100644 index 0000000..3ad242f --- /dev/null +++ b/docs/src/examples/foldabletable/FoldableTableWithoutHeader.js @@ -0,0 +1,64 @@ +import React from 'react'; +import ReactTable from '../../../../lib/index'; +import FoldableTableHOC from '../../../../lib/hoc/foldableTable'; + +const FoldableTable = FoldableTableHOC(ReactTable); + +export default class FoldableTableWithoutHeader extends React.Component { + + getData = () => [{ + "id": 1, + "first_name": "Jeanette", + "last_name": "Penddreth", + "email": "jpenddreth0@census.gov", + "gender": "Female", + "ip_address": "26.58.193.2" + }, { + "id": 2, + "first_name": "Giavani", + "last_name": "Frediani", + "email": "gfrediani1@senate.gov", + "gender": "Male", + "ip_address": "229.179.4.212" + }, { + "id": 3, + "first_name": "Noell", + "last_name": "Bea", + "email": "nbea2@imageshack.us", + "gender": "Female", + "ip_address": "180.66.162.255" + }, { + "id": 4, + "first_name": "Willard", + "last_name": "Valek", + "email": "wvalek3@vk.com", + "gender": "Male", + "ip_address": "67.76.188.26" + }]; + + render() { + return + } +} diff --git a/docs/src/examples/foldabletable/index.js b/docs/src/examples/foldabletable/index.js new file mode 100644 index 0000000..10f0448 --- /dev/null +++ b/docs/src/examples/foldabletable/index.js @@ -0,0 +1,30 @@ + +import React from 'react' +import '../../../../react-table.css' + +import FoldableTableWithHeader from './FoldableTableWithHeader' +import FoldableTableWithoutHeader from './FoldableTableWithoutHeader' +import FoldableTableCustomState from './FoldableTableCustomState' + +class FaldableComponentTest extends React.Component { + render() { + + return ( +
+

- Sample With Header Columns

+
+ +

+

- Sample With Normal Columns

+
+ +

+

- Custom State and selectedTable

+
+ +
+ ) + } +} + +export default FaldableComponentTest diff --git a/docs/src/examples/index.js b/docs/src/examples/index.js index 8e50611..69b737a 100644 --- a/docs/src/examples/index.js +++ b/docs/src/examples/index.js @@ -1,7 +1,13 @@ /* eslint-disable */ -import TreeTable from "./treetable"; -import SelectTable from "./selecttable"; -import SelectTreeTable from "./selecttreetable"; +import TreeTable from './treetable' +import SelectTable from './selecttable' +import SelectTreeTable from './selecttreetable' +import FoldableTable from './foldabletable'; -export { TreeTable, SelectTable, SelectTreeTable }; +export { + TreeTable, + SelectTable, + SelectTreeTable, + FoldableTable +} diff --git a/src/hoc/README.md b/src/hoc/README.md index 1b8f85e..954fc8e 100644 --- a/src/hoc/README.md +++ b/src/hoc/README.md @@ -103,6 +103,107 @@ 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. +### FoldableTable +FoldableTable is a HOC that make the columns are foldable. The reason I developed this HOC because when working on the real project related to the financial which display so many columns for validation and comparison. + +So foldable columns allow users to temporary hidden the unwanted to columns so that they can focus on the data that they want to see. + +#### How it work +```javascfript +import ReactTable from 'react-table' +import FoldableTableHOC from 'react-table/lib/hoc/foldableTable' + +const FoldableTable = FoldableTableHOC(ReactTable); +``` +It will scan all the columns which `foldable` is `true` and apply the foldable column feature. This feature will work for both normal columns and header columns as samples below. + +- With Header Columns +```javascript +render(){ + return +} +``` + +![With Header Columns](https://raw.githubusercontent.com/baoduy/Images/master/Wordpress/JavaScripts/react-table%20foldableHOC/FoldableTable%20With%20Header.gif) + +- With Nornal Columns +```javascript +render() { + return +} +``` + +![With Normal Columns](https://raw.githubusercontent.com/baoduy/Images/master/Wordpress/JavaScripts/react-table%20foldableHOC/FoldableTable%20Without%20Header.gif) + +- The `FoldableTable` also fully compatible with existing HOCs, below is with selectTableHOC. + +![With Normal Columns](https://raw.githubusercontent.com/baoduy/Images/master/Wordpress/JavaScripts/react-table%20foldableHOC/FoldableTable%20With%20selectTable.gif) + +#### State management +If you would like to manage the state of FoldableTable, then add the following codes. +```javascript +render() { + return this.setState(p => { return { folded: newFolded } })} + folded={this.state.folded} + /> +} +``` +#### Custom Compoments + - FoldIconComponent: to render the Icon of buttons. + - FoldButtonComponent: to render the folding buttons for each Column. + With default rendering as below. +```javascript +const defaultFoldIconComponent = ({ collapsed }) => { + //Render your Icon here +} + +const defaultFoldButtonComponent = ({ header, collapsed, icon, onClick }) => { + //Render your button here. +} +``` + ## 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 diff --git a/src/hoc/foldableTable/index.js b/src/hoc/foldableTable/index.js new file mode 100644 index 0000000..b6263ea --- /dev/null +++ b/src/hoc/foldableTable/index.js @@ -0,0 +1,225 @@ +import React from 'react'; +import left from './left.svg'; +import right from './right.svg'; + +const defaultFoldIconComponent = ({ collapsed }) => { + const style = { width: 25 }; + + if (collapsed) + return right + return left +} + +const defaultFoldButtonComponent = ({ header, collapsed, icon, onClick }) => { + const style = { + marginLeft: "0px", + marginTop: "-5px", + marginBottom: "-8px", + float: "left", + cursor: "pointer" + }; + + return (
+
+ {icon} +
+ {!collapsed &&
{header}
} +
); +} + +export default (ReactTable) => { + + const wrapper = class RTFoldableTable extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + folded: props.onFoldChange ? undefined : {}, + resized: props.resized || [] + }; + } + + componentWillReceiveProps(newProps) { + if (this.state.resized !== newProps.resized) + this.setState(p => { return { resized: newProps.resized } }); + } + + onResizedChange = resized => { + const { onResizedChange } = this.props; + if (onResizedChange) + onResizedChange(resized); + else this.setState(p => { return { resized } }); + } + + removeResized = column => { + const { id } = column; + if (!id) return; + + const { resized } = this.state; + if (!resized) return; + + const rs = resized.find(r => r.id === id); + if (!rs) return; + + const newResized = resized.filter(r => r !== rs); + this.onResizedChange(newResized); + } + + // this is so we can expose the underlying ReactTable. + getWrappedInstance = () => { + if (!this.wrappedInstance) console.warn('RTFoldableTable - No wrapped instance'); + if (this.wrappedInstance.getWrappedInstance) return this.wrappedInstance.getWrappedInstance(); + else return this.wrappedInstance + } + + getCopiedKey = key => { + const { foldableOriginalKey } = this.props; + return `${foldableOriginalKey}${key}`; + } + + copyOriginals = column => { + const { FoldedColumn } = this.props; + + //Stop copy if the column already copied + if (column.original_Header) return; + + Object.keys(FoldedColumn).forEach(k => { + const copiedKey = this.getCopiedKey(k); + + if (k === "Cell") + column[copiedKey] = column[k] ? column[k] : c => c.value; + else column[copiedKey] = column[k]; + }); + + //Copy sub Columns + if (column.columns && !column.original_Columns) + column.original_Columns = column.columns; + + //Copy Header + if (!column.original_Header) + column.original_Header = column.Header; + } + + restoreToOriginal = column => { + const { FoldedColumn } = this.props; + + Object.keys(FoldedColumn).forEach(k => { + //ignore header as handling by foldableHeaderRender + if (k === "Header") return; + + const copiedKey = this.getCopiedKey(k); + column[k] = column[copiedKey]; + }); + + if (column.columns && column.original_Columns) + column.columns = column.original_Columns; + } + + getState = () => this.props.onFoldChange ? this.props.folded : this.state.folded; + + isFolded = col => { + const folded = this.getState(); + return folded[col.id] === true; + } + + foldingHandler = col => { + if (!col || !col.id) return; + + const { onFoldChange } = this.props; + const folded = this.getState(); + const { id } = col; + + let newFold = Object.assign({}, folded); + newFold[id] = !newFold[id]; + + //Remove the Resized if have + this.removeResized(col); + + if (onFoldChange) + onFoldChange(newFold); + else this.setState(previous => { return { folded: newFold }; }); + } + + foldableHeaderRender = (cell) => { + const { FoldButtonComponent, FoldIconComponent } = this.props; + const { column } = cell; + const collapsed = this.isFolded(column); + const icon = React.createElement(FoldIconComponent, { collapsed }); + const onClick = () => this.foldingHandler(column); + + return React.createElement(FoldButtonComponent, { header: column.original_Header, collapsed, icon, onClick }); + } + + applyFoldableForColumn = column => { + const collapsed = this.isFolded(column); + const { FoldedColumn } = this.props; + + //Handle Column Header + if (column.columns) { + if (collapsed) { + column.columns = [FoldedColumn]; + column.width = FoldedColumn.width; + column.style = FoldedColumn.style; + } + else this.restoreToOriginal(column); + } + //Handle Normal Column. + else if (collapsed) + column = Object.assign(column, FoldedColumn); + else { + this.restoreToOriginal(column); + } + } + + applyFoldableForColumns = columns => { + return columns.map((col, index) => { + if (!col.foldable) return col; + + //If col don't have id then generate id based on index + if (!col.id) + col.id = `col_${index}`; + + this.copyOriginals(col); + //Replace current header with internal header render. + col.Header = c => this.foldableHeaderRender(c); + //apply foldable + this.applyFoldableForColumn(col); + + //return the new column out + return col; + }); + } + + render() { + const { columns: originalCols, FoldButtonComponent, FoldIconComponent, FoldedColumn, ...rest } = this.props; + const columns = this.applyFoldableForColumns([...originalCols]); + + const extra = { + columns, + onResizedChange: this.onResizedChange, + resized: this.state.resized + }; + + return ( + this.wrappedInstance = r} /> + ) + } + } + + wrapper.displayName = 'RTFoldableTable'; + wrapper.defaultProps = + { + FoldIconComponent: defaultFoldIconComponent, + FoldButtonComponent: defaultFoldButtonComponent, + foldableOriginalKey: 'original_', + FoldedColumn: { + Cell: c => '', + width: 30, + sortable: false, + resizable: false, + filterable: false, + } + } + + return wrapper; +} \ No newline at end of file diff --git a/src/hoc/foldableTable/left.svg b/src/hoc/foldableTable/left.svg new file mode 100644 index 0000000..f0383f7 --- /dev/null +++ b/src/hoc/foldableTable/left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/hoc/foldableTable/right.svg b/src/hoc/foldableTable/right.svg new file mode 100644 index 0000000..5088e25 --- /dev/null +++ b/src/hoc/foldableTable/right.svg @@ -0,0 +1 @@ + \ No newline at end of file