Repository: gregnb/mui-datatables
Pull request #280 (2018-11-19 09:35:48)

[Feature] Custom toolbar select with imperative setSelectedRows
Added two additional parameters to `customToolbarSelect`: 
- `displayData` - an array of actual displaying data in the table. It's may be used for calculating next selected rows
- `setSelectedRows` - a function, that receives `selectedRows` (array of numbers - indexes of selected rows). It's may be used to set imperatively selected rows after user click on action button from your customToolbarSelect.

Also, I've updated documentation and example `customize-toolbarselect` (need to deploy to firebase).

This PR will cover #266 

coveralls: 
[![Coverage Status](https://coveralls.io/builds/20175835/badge)](https://coveralls.io/builds/20175835)

Coverage increased (+0.4%) to 78.018% when pulling **b9786c5ce70279cbbb3f4c597907732d0483ea39 on DTupalov:feature/custom-toolbar-select-setSelectedRows** into **a35d95939a4f675f34afaedb06b560bd392286b3 on gregnb:master**.

gregnb: Looks good man! Thanks for the PR!

Patch:
diff --git a/README.md b/README.md
index 0b679fe..ef32cab 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,7 @@ The component accepts the following props:
 |**`selectableRows`**|boolean|true|Enable/disable row selection
 |**`resizableColumns`**|boolean|false|Enable/disable resizable columns
 |**`customToolbar`**|function||Render a custom toolbar
-|**`customToolbarSelect`**|function||Render a custom selected rows toolbar
+|**`customToolbarSelect`**|function||Render a custom selected rows toolbar. `function(selectedRows, displayData, setSelectedRows) => void`
 |**`customFooter`**|function||Render a custom table footer. `function(count, page, rowsPerPage, changeRowsPerPage, changePage) => string`&#124;` React Component`
 |**`customSort`**|function||Override default sorting with custom function. `function(data: array, colIndex: number, order: string) => array`
 |**`caseSensitive `**|boolean|false|Enable/disable case sensitivity for search
@@ -167,6 +167,19 @@ The component accepts the following props:
 |**`onColumnViewChange`**|function||Callback function that triggers when a column view has been changed. `function(changedColumn: string, action: string) => void`
 |**`onTableChange`**|function||Callback function that triggers when table state has changed. `function(action: string, tableState: object) => void`
 
+`customToolbarSelect` is called with these arguments:
+
+```js
+function(
+    selectedRows:  
+      {
+        data: Array<{index: number, dataIndex: number}>, 
+        lookup: Object
+      }, 
+    displayData: Array<{data: any, dataIndex: number}>,
+    setSelectedRows: (nextSelectedRows: number[]) => void
+  )
+```
 
 ## Customize Columns
 
diff --git a/examples/customize-toolbarselect/CustomToolbarSelect.js b/examples/customize-toolbarselect/CustomToolbarSelect.js
index ff34261..3e7521c 100644
--- a/examples/customize-toolbarselect/CustomToolbarSelect.js
+++ b/examples/customize-toolbarselect/CustomToolbarSelect.js
@@ -1,8 +1,9 @@
 import React from "react";
 import IconButton from "@material-ui/core/IconButton";
 import Tooltip from "@material-ui/core/Tooltip";
-import DeleteIcon from "@material-ui/icons/Delete";
-import FilterIcon from "@material-ui/icons/FilterList";
+import CompareArrowsIcon from "@material-ui/icons/CompareArrows";
+import IndeterminateCheckBoxIcon from "@material-ui/icons/IndeterminateCheckBox";
+import BlockIcon from "@material-ui/icons/Block";
 import { withStyles } from "@material-ui/core/styles";
 
 const defaultToolbarSelectStyles = {
@@ -13,36 +14,58 @@ const defaultToolbarSelectStyles = {
     position: "relative",
     transform: "translateY(-50%)",
   },
-  deleteIcon: {
+  icon: {
     color: "#000",
   },
+  inverseIcon: {
+    transform: "rotate(90deg)",
+  },
 };
 
 class CustomToolbarSelect extends React.Component {
+  handleClickInverseSelection = () => {
+    const nextSelectedRows = this.props.displayData.reduce((nextSelectedRows, _, index) => {
+      if (!this.props.selectedRows.data.find(selectedRow => selectedRow.index === index)) {
+        nextSelectedRows.push(index);
+      }
 
-  handleClick = () => {
-    console.log("click! current selected rows", this.props.selectedRows);
-  }
+      return nextSelectedRows;
+    }, []);
+
+    this.props.setSelectedRows(nextSelectedRows);
+  };
+
+  handleClickDeselectAll = () => {
+    this.props.setSelectedRows([]);
+  };
+
+  handleClickBlockSelected = () => {
+    console.log(`block users with dataIndexes: ${this.props.selectedRows.data.map(row => row.dataIndex)}`);
+  };
 
   render() {
     const { classes } = this.props;
 
     return (
       <div className={"custom-toolbar-select"}>
-        <Tooltip title={"icon 2"}>
-          <IconButton className={classes.iconButton} onClick={this.handleClick}>
-            <FilterIcon className={classes.deleteIcon} />
+        <Tooltip title={"Deselect ALL"}>
+          <IconButton className={classes.iconButton} onClick={this.handleClickDeselectAll}>
+            <IndeterminateCheckBoxIcon className={classes.icon} />
+          </IconButton>
+        </Tooltip>
+        <Tooltip title={"Inverse selection"}>
+          <IconButton className={classes.iconButton} onClick={this.handleClickInverseSelection}>
+            <CompareArrowsIcon className={[classes.icon, classes.inverseIcon].join(" ")} />
           </IconButton>
         </Tooltip>
-        <Tooltip title={"icon 1"}>
-          <IconButton className={classes.iconButton} onClick={this.handleClick}>
-            <DeleteIcon className={classes.deleteIcon} />
+        <Tooltip title={"Block selected"}>
+          <IconButton className={classes.iconButton} onClick={this.handleClickBlockSelected}>
+            <BlockIcon className={classes.icon} />
           </IconButton>
         </Tooltip>
       </div>
     );
   }
-
 }
 
 export default withStyles(defaultToolbarSelectStyles, { name: "CustomToolbarSelect" })(CustomToolbarSelect);
diff --git a/examples/customize-toolbarselect/index.js b/examples/customize-toolbarselect/index.js
index fdd072d..7dd528c 100644
--- a/examples/customize-toolbarselect/index.js
+++ b/examples/customize-toolbarselect/index.js
@@ -4,14 +4,12 @@ import MUIDataTable from "../../src/";
 import CustomToolbarSelect from "./CustomToolbarSelect";
 
 class Example extends React.Component {
-
   render() {
-
     const columns = ["Name", "Title", "Location", "Age", "Salary"];
 
     let data = [
       ["Gabby George", "Business Analyst", "Minneapolis", 30, 100000],
-      ["Aiden Lloyd", "Business Consultant", "Dallas",  55, 200000],
+      ["Aiden Lloyd", "Business Consultant", "Dallas", 55, 200000],
       ["Jaden Collins", "Attorney", "Santa Ana", 27, 500000],
       ["Franky Rees", "Business Analyst", "St. Petersburg", 22, 50000],
       ["Aaren Rose", "Business Consultant", "Toledo", 28, 75000],
@@ -39,26 +37,21 @@ class Example extends React.Component {
       ["Franky Miles", "Industrial Analyst", "Buffalo", 49, 190000],
       ["Glen Nixon", "Corporate Counselor", "Arlington", 44, 80000],
       ["Gabby Strickland", "Business Process Consultant", "Scottsdale", 26, 45000],
-      ["Mason Ray", "Computer Scientist", "San Francisco", 39, 142000]
+      ["Mason Ray", "Computer Scientist", "San Francisco", 39, 142000],
     ];
 
-    for (let i = 0; i < 100000; i++) {
-      data.push(["Mason Ray", "Computer Scientist", "San Francisco", 39, 142000]);
-    }
-
     const options = {
       filter: true,
       selectableRows: true,
-      filterType: 'dropdown',
-      responsive: 'stacked',
+      filterType: "dropdown",
+      responsive: "stacked",
       rowsPerPage: 10,
-      customToolbarSelect: (selectedRows) => <CustomToolbarSelect selectedRows={selectedRows} />
+      customToolbarSelect: (selectedRows, displayData, setSelectedRows) => (
+        <CustomToolbarSelect selectedRows={selectedRows} displayData={displayData} setSelectedRows={setSelectedRows} />
+      ),
     };
 
-    return (
-      <MUIDataTable title={"ACME Employee list"} data={data} columns={columns} options={options} />
-    );
-
+    return <MUIDataTable title={"ACME Employee list"} data={data} columns={columns} options={options} />;
   }
 }
 
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index e27ce23..9f652d9 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -756,6 +756,23 @@ class MUIDataTable extends React.Component {
           }
         },
       );
+    } else if (type === "custom") {
+      const { displayData } = this.state;
+
+      const data = value.map(row => ({ index: row, dataIndex: displayData[row].dataIndex }));
+      const lookup = this.buildSelectedMap(data);
+
+      this.setState(
+        {
+          selectedRows: { data, lookup },
+        },
+        () => {
+          this.setTableAction("rowsSelect");
+          if (this.options.onRowsSelect) {
+            this.options.onRowsSelect(this.state.selectedRows.data, this.state.selectedRows.data);
+          }
+        },
+      );
     }
   };
 
@@ -828,6 +845,8 @@ class MUIDataTable extends React.Component {
             options={this.options}
             selectedRows={selectedRows}
             onRowsDelete={this.selectRowDelete}
+            displayData={displayData}
+            selectRowUpdate={this.selectRowUpdate}
           />
         ) : (
           <MUIDataTableToolbar
diff --git a/src/MUIDataTableToolbarSelect.js b/src/MUIDataTableToolbarSelect.js
index 9bb872e..7db8d63 100644
--- a/src/MUIDataTableToolbarSelect.js
+++ b/src/MUIDataTableToolbarSelect.js
@@ -45,8 +45,23 @@ class MUIDataTableToolbarSelect extends React.Component {
     classes: PropTypes.object,
   };
 
+  /**
+   * @param {number[]} selectedRows Array of rows indexes that are selected, e.g. [0, 2] will select first and third rows in table
+   */
+  handleCustomSelectedRows = selectedRows => {
+    if (!Array.isArray(selectedRows)) {
+      throw new TypeError(`"selectedRows" must be an "array", but it's "${typeof selectedRows}"`);
+    }
+
+    if (selectedRows.some(row => typeof row !== "number")) {
+      throw new TypeError(`Array "selectedRows" must contain only numbers`);
+    }
+
+    this.props.selectRowUpdate("custom", selectedRows);
+  };
+
   render() {
-    const { classes, onRowsDelete, selectedRows, options } = this.props;
+    const { classes, onRowsDelete, selectedRows, options, displayData } = this.props;
     const textLabels = options.textLabels.selectedRows;
 
     return (
@@ -57,7 +72,7 @@ class MUIDataTableToolbarSelect extends React.Component {
           </Typography>
         </div>
         {options.customToolbarSelect ? (
-          options.customToolbarSelect(selectedRows)
+          options.customToolbarSelect(selectedRows, displayData, this.handleCustomSelectedRows)
         ) : (
           <Tooltip title={textLabels.delete}>
             <IconButton className={classes.iconButton} onClick={onRowsDelete} aria-label={textLabels.deleteAria}>


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index 390979a..8779ed9 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -432,6 +432,19 @@ describe("<MUIDataTable />", function() {
     assert.deepEqual(state.selectedRows.data, [0]);
   });
 
+  it("should update selectedRows when calling selectRowUpdate method with type=custom", () => {
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.selectRowUpdate("custom", [0, 3]);
+    shallowWrapper.update();
+
+    const state = shallowWrapper.state();
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 3, dataIndex: 3 }];
+
+    assert.deepEqual(state.selectedRows.data, expectedResult);
+  });
+
   it("should update value when calling updateValue method in customBodyRender", () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />).dive();
     const instance = shallowWrapper.instance();
@@ -442,6 +455,7 @@ describe("<MUIDataTable />", function() {
     const state = shallowWrapper.state();
     assert.deepEqual(state.data[0].data[2], "Las Vegas");
   });
+
   it("should call onTableChange when calling selectRowUpdate method with type=head", () => {
     const options = { selectableRows: true, onTableChange: spy() };
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
@@ -460,6 +474,7 @@ describe("<MUIDataTable />", function() {
     assert.deepEqual(state.selectedRows.data, expectedResult);
     assert.strictEqual(options.onTableChange.callCount, 1);
   });
+
   it("should call onTableChange when calling selectRowUpdate method with type=cell", () => {
     const options = { selectableRows: true, onTableChange: spy() };
 
@@ -473,4 +488,19 @@ describe("<MUIDataTable />", function() {
     assert.deepEqual(state.selectedRows.data, [0]);
     assert.strictEqual(options.onTableChange.callCount, 1);
   });
+
+  it("should call onTableChange when calling selectRowUpdate method with type=custom", () => {
+    const options = { selectableRows: true, onTableChange: spy() };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.selectRowUpdate("custom", [0, 3]);
+    shallowWrapper.update();
+
+    const state = shallowWrapper.state();
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 3, dataIndex: 3 }];
+
+    assert.deepEqual(state.selectedRows.data, expectedResult);
+    assert.strictEqual(options.onTableChange.callCount, 1);
+  });
 });
diff --git a/test/MUIDataTableToolbarSelect.test.js b/test/MUIDataTableToolbarSelect.test.js
index e773145..4e8c3a7 100644
--- a/test/MUIDataTableToolbarSelect.test.js
+++ b/test/MUIDataTableToolbarSelect.test.js
@@ -1,5 +1,5 @@
 import React from "react";
-import { spy, stub } from "sinon";
+import { match, spy, stub } from "sinon";
 import { mount, shallow } from "enzyme";
 import { assert, expect, should } from "chai";
 import DeleteIcon from "@material-ui/icons/Delete";
@@ -18,4 +18,78 @@ describe("<MUIDataTableSelectCell />", function() {
     const actualResult = mountWrapper.find(DeleteIcon);
     assert.strictEqual(actualResult.length, 1);
   });
+
+  it("should call customToolbarSelect with 3 arguments", () => {
+    const onRowsDelete = () => {};
+    const customToolbarSelect = spy();
+    const selectedRows = { data: [1] };
+    const displayData = [1];
+
+    const mountWrapper = mount(
+      <MUIDataTableToolbarSelect
+        options={{ textLabels, customToolbarSelect }}
+        selectedRows={selectedRows}
+        onRowsDelete={onRowsDelete}
+        displayData={displayData}
+      />,
+    );
+
+    assert.strictEqual(customToolbarSelect.calledWith(selectedRows, displayData, match.typeOf("function")), true);
+  });
+
+  it("should throw TypeError if selectedRows is not an array of numbers", done => {
+    const onRowsDelete = () => {};
+    const selectRowUpdate = () => {};
+    const customToolbarSelect = (_, __, setSelectedRows) => {
+      const spySetSelectedRows = spy(setSelectedRows);
+      try {
+        spySetSelectedRows("");
+      } catch (error) {
+        //do nothing
+      }
+      try {
+        spySetSelectedRows(["1"]);
+      } catch (error) {
+        //do nothing
+      }
+
+      spySetSelectedRows.exceptions.forEach(error => assert.strictEqual(error instanceof TypeError, true));
+
+      done();
+    };
+    const selectedRows = { data: [1] };
+    const displayData = [1];
+
+    const mountWrapper = mount(
+      <MUIDataTableToolbarSelect
+        options={{ textLabels, customToolbarSelect }}
+        selectedRows={selectedRows}
+        onRowsDelete={onRowsDelete}
+        displayData={displayData}
+        selectRowUpdate={selectRowUpdate}
+      />,
+    );
+  });
+
+  it("should call selectRowUpdate when customToolbarSelect passed and setSelectedRows was called", () => {
+    const onRowsDelete = () => {};
+    const selectRowUpdate = spy();
+    const customToolbarSelect = (_, __, setSelectedRows) => {
+      setSelectedRows([1]);
+    };
+    const selectedRows = { data: [1] };
+    const displayData = [1];
+
+    const mountWrapper = mount(
+      <MUIDataTableToolbarSelect
+        options={{ textLabels, customToolbarSelect }}
+        selectedRows={selectedRows}
+        onRowsDelete={onRowsDelete}
+        displayData={displayData}
+        selectRowUpdate={selectRowUpdate}
+      />,
+    );
+
+    assert.strictEqual(selectRowUpdate.calledOnce, true);
+  });
 });