Skip to content

Commit

Permalink
[Feature] Custom toolbar select with imperative setSelectedRows (#280)
Browse files Browse the repository at this point in the history
* feat(customToolbarSelect): add imperative manage of selection

Feature request #266

* doc(customToolbarSelect): update README and example

Feature request #266
  • Loading branch information
DTupalov authored and gregnb committed Nov 19, 2018
1 parent a35d959 commit 149b18e
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 32 deletions.
15 changes: 14 additions & 1 deletion README.md
Expand Up @@ -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`|` 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
Expand Down Expand Up @@ -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

Expand Down
49 changes: 36 additions & 13 deletions 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 = {
Expand All @@ -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);
23 changes: 8 additions & 15 deletions examples/customize-toolbarselect/index.js
Expand Up @@ -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],
Expand Down Expand Up @@ -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} />;
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/MUIDataTable.js
Expand Up @@ -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);
}
},
);
}
};

Expand Down Expand Up @@ -828,6 +845,8 @@ class MUIDataTable extends React.Component {
options={this.options}
selectedRows={selectedRows}
onRowsDelete={this.selectRowDelete}
displayData={displayData}
selectRowUpdate={this.selectRowUpdate}
/>
) : (
<MUIDataTableToolbar
Expand Down
19 changes: 17 additions & 2 deletions src/MUIDataTableToolbarSelect.js
Expand Up @@ -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 (
Expand All @@ -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}>
Expand Down
30 changes: 30 additions & 0 deletions test/MUIDataTable.test.js
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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() };

Expand All @@ -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);
});
});
76 changes: 75 additions & 1 deletion 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";
Expand All @@ -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);
});
});

0 comments on commit 149b18e

Please sign in to comment.