diff --git a/docs/row-selection.md b/docs/row-selection.md index 832ef1d..0aa5f2c 100644 --- a/docs/row-selection.md +++ b/docs/row-selection.md @@ -15,6 +15,8 @@ The following are available properties in `selectRow`: * [nonSelectable)](#nonSelectable) * [clickToSelect)](#clickToSelect) * [clickToEdit](#clickToEdit) +* [onSelect](#onSelect) +* [onSelectAll](#onSelectAll) #### Optional @@ -144,4 +146,29 @@ const selectRow = { clickToSelect: true clickToEdit: true }; -``` \ No newline at end of file +``` + +# selectRow.onSelect - [Function] +This callback function will be called when a row is select/unselect and pass following three arguments: +`row`, `isSelect` and `rowIndex`. + +```js +const selectRow = { + mode: 'checkbox', + onSelect: (row, isSelect, rowIndex) => { + // ... + } +}; +``` + +# selectRow.onSelectAll - [Function] +This callback function will be called when select/unselect all and it only work when you configure [`selectRow.mode`](#mode) as `checkbox`. + +```js +const selectRow = { + mode: 'checkbox', + onSelectAll: (isSelect, results) => { + // ... + } +}; +``` diff --git a/packages/react-bootstrap-table2-example/examples/row-selection/selection-hooks.js b/packages/react-bootstrap-table2-example/examples/row-selection/selection-hooks.js new file mode 100644 index 0000000..fd94d6b --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-selection/selection-hooks.js @@ -0,0 +1,66 @@ + +/* eslint no-console: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table2'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const selectRow = { + mode: 'checkbox', + clickToSelect: true, + onSelect: (row, isSelect, rowIndex) => { + console.log(row.id); + console.log(isSelect); + console.log(rowIndex); + }, + onSelectAll: (isSelect, rows) => { + console.log(isSelect); + console.log(rows); + } +}; + +const sourceCode = `\ +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const selectRow = { + mode: 'checkbox', + clickToSelect: true +}; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index 6598dd6..31a03c1 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -59,6 +59,7 @@ import SelectionStyleTable from 'examples/row-selection/selection-style'; import SelectionClassTable from 'examples/row-selection/selection-class'; import NonSelectableRowsTable from 'examples/row-selection/non-selectable-rows'; import SelectionBgColorTable from 'examples/row-selection/selection-bgcolor'; +import SelectionHooks from 'examples/row-selection/selection-hooks'; // css style import 'bootstrap/dist/css/bootstrap.min.css'; @@ -126,4 +127,5 @@ storiesOf('Row Selection', module) .add('Selection Style', () => ) .add('Selection Class', () => ) .add('Selection Background Color', () => ) - .add('Not Selectabled Rows', () => ); + .add('Not Selectabled Rows', () => ) + .add('Selection Hooks', () => ); diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index 11530c8..bcbf5cf 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -130,6 +130,8 @@ BootstrapTable.propTypes = { mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired, clickToSelect: PropTypes.bool, clickToEdit: PropTypes.bool, + onSelect: PropTypes.func, + onSelectAll: PropTypes.func, style: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), classes: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), nonSelectable: PropTypes.array, diff --git a/packages/react-bootstrap-table2/src/row-selection/selection-cell.js b/packages/react-bootstrap-table2/src/row-selection/selection-cell.js index fa41f3a..bc7e0c5 100644 --- a/packages/react-bootstrap-table2/src/row-selection/selection-cell.js +++ b/packages/react-bootstrap-table2/src/row-selection/selection-cell.js @@ -12,7 +12,8 @@ export default class SelectionCell extends Component { rowKey: PropTypes.any, selected: PropTypes.bool, onRowSelect: PropTypes.func, - disabled: PropTypes.bool + disabled: PropTypes.bool, + rowIndex: PropTypes.number } constructor() { @@ -32,7 +33,8 @@ export default class SelectionCell extends Component { rowKey, selected, onRowSelect, - disabled + disabled, + rowIndex } = this.props; if (disabled) return; @@ -41,7 +43,7 @@ export default class SelectionCell extends Component { ? true : !selected; - onRowSelect(rowKey, checked); + onRowSelect(rowKey, checked, rowIndex); } render() { diff --git a/packages/react-bootstrap-table2/src/row-selection/wrapper.js b/packages/react-bootstrap-table2/src/row-selection/wrapper.js index dad9d6e..0e6b1c7 100644 --- a/packages/react-bootstrap-table2/src/row-selection/wrapper.js +++ b/packages/react-bootstrap-table2/src/row-selection/wrapper.js @@ -21,8 +21,8 @@ class RowSelectionWrapper extends Component { * @param {String} rowKey - row key of what was selected. * @param {Boolean} checked - next checked status of input button. */ - handleRowSelect(rowKey, checked) { - const { selectRow: { mode }, store } = this.props; + handleRowSelect(rowKey, checked, rowIndex) { + const { selectRow: { mode, onSelect }, store } = this.props; const { ROW_SELECT_SINGLE } = Const; let currSelected = [...store.getSelectedRowKeys()]; @@ -37,6 +37,11 @@ class RowSelectionWrapper extends Component { store.setSelectedRowKeys(currSelected); + if (onSelect) { + const row = store.getRowByRowId(rowKey); + onSelect(row, checked, rowIndex); + } + this.setState(() => ({ selectedRowKeys: currSelected })); @@ -47,19 +52,26 @@ class RowSelectionWrapper extends Component { * @param {Boolean} option - customized result for all rows selection */ handleAllRowsSelect(option) { - const { store, selectRow } = this.props; - const selected = store.isAnySelectedRow(selectRow.nonSelectable); + const { store, selectRow: { + onSelectAll, + nonSelectable + } } = this.props; + const selected = store.isAnySelectedRow(nonSelectable); // set next status of all row selected by store.selected or customizing by user. const result = option || !selected; const currSelected = result ? - store.selectAllRows(selectRow.nonSelectable) : - store.cleanSelectedRows(selectRow.nonSelectable); + store.selectAllRows(nonSelectable) : + store.cleanSelectedRows(nonSelectable); store.setSelectedRowKeys(currSelected); + if (onSelectAll) { + onSelectAll(result, store.getSelectedRows()); + } + this.setState(() => ({ selectedRowKeys: currSelected })); diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js index 23a8cf2..a53682e 100644 --- a/packages/react-bootstrap-table2/src/row.js +++ b/packages/react-bootstrap-table2/src/row.js @@ -21,6 +21,7 @@ class Row extends Component { selected, keyField, selectable, + rowIndex, selectRow: { onRowSelect, clickToEdit @@ -33,12 +34,12 @@ class Row extends Component { this.clickNum += 1; _.debounce(() => { if (this.clickNum === 1) { - onRowSelect(key, !selected); + onRowSelect(key, !selected, rowIndex); } this.clickNum = 0; }, Const.DELAY_FOR_DBCLICK)(); } else { - onRowSelect(key, !selected); + onRowSelect(key, !selected, rowIndex); } } } @@ -83,6 +84,7 @@ class Row extends Component { diff --git a/packages/react-bootstrap-table2/src/store/base.js b/packages/react-bootstrap-table2/src/store/base.js index eb435d1..509dc63 100644 --- a/packages/react-bootstrap-table2/src/store/base.js +++ b/packages/react-bootstrap-table2/src/store/base.js @@ -50,6 +50,10 @@ export default class Store { this.selected = selectedKeys; } + getSelectedRows() { + return this.selected.map(k => this.getRowByRowId(k)); + } + getSelectedRowKeys() { return this.selected; } diff --git a/packages/react-bootstrap-table2/test/body.test.js b/packages/react-bootstrap-table2/test/body.test.js index 717e34e..2e98068 100644 --- a/packages/react-bootstrap-table2/test/body.test.js +++ b/packages/react-bootstrap-table2/test/body.test.js @@ -318,7 +318,7 @@ describe('Body', () => { it('should calling selectRow.bgColor callback correctly', () => { expect(bgColorCallBack.calledOnce).toBeTruthy(); - expect(bgColorCallBack.calledWith(data[0]), 1); + expect(bgColorCallBack.calledWith(data[0]), 1).toBeTruthy(); }); it('should render Row component with correct style.backgroundColor prop', () => { diff --git a/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js b/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js index 5c92145..bcb5799 100644 --- a/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js +++ b/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js @@ -6,6 +6,7 @@ import SelectionCell from '../../src/row-selection/selection-cell'; describe('', () => { const mode = 'checkbox'; + const rowIndex = 1; let wrapper; @@ -56,6 +57,7 @@ describe('', () => { selected rowKey={ rowKey } mode={ mode } + rowIndex={ rowIndex } onRowSelect={ mockOnRowSelect } /> ); @@ -69,7 +71,7 @@ describe('', () => { it('should calling onRowSelect callback correctly', () => { expect(mockOnRowSelect.calledOnce).toBe(true); expect( - mockOnRowSelect.calledWith(rowKey, !selected) + mockOnRowSelect.calledWith(rowKey, !selected, rowIndex) ).toBe(true); }); }); @@ -81,6 +83,7 @@ describe('', () => { selected rowKey={ rowKey } mode={ mode } + rowIndex={ rowIndex } onRowSelect={ mockOnRowSelect } disabled /> @@ -104,6 +107,7 @@ describe('', () => { selected rowKey={ rowKey } mode="radio" + rowIndex={ rowIndex } onRowSelect={ mockOnRowSelect } /> ); @@ -113,12 +117,12 @@ describe('', () => { // first click wrapper.find('td').simulate('click'); expect(mockOnRowSelect.callCount).toBe(1); - expect(mockOnRowSelect.calledWith(rowKey, true)).toBe(true); + expect(mockOnRowSelect.calledWith(rowKey, true, rowIndex)).toBe(true); // second click wrapper.find('td').simulate('click'); expect(mockOnRowSelect.callCount).toBe(2); - expect(mockOnRowSelect.calledWith(rowKey, true)).toBe(true); + expect(mockOnRowSelect.calledWith(rowKey, true, rowIndex)).toBe(true); }); }); @@ -128,6 +132,7 @@ describe('', () => { ); @@ -138,13 +143,13 @@ describe('', () => { wrapper.setProps({ selected: true }); wrapper.find('td').simulate('click'); expect(mockOnRowSelect.callCount).toBe(1); - expect(mockOnRowSelect.calledWith(rowKey, false)).toBe(true); + expect(mockOnRowSelect.calledWith(rowKey, false, rowIndex)).toBe(true); // second click wrapper.setProps({ selected: false }); wrapper.find('td').simulate('click'); expect(mockOnRowSelect.callCount).toBe(2); - expect(mockOnRowSelect.calledWith(rowKey, true)).toBe(true); + expect(mockOnRowSelect.calledWith(rowKey, true, rowIndex)).toBe(true); }); }); }); @@ -158,6 +163,7 @@ describe('', () => { ); @@ -176,6 +182,7 @@ describe('', () => { diff --git a/packages/react-bootstrap-table2/test/row-selection/wrapper.test.js b/packages/react-bootstrap-table2/test/row-selection/wrapper.test.js index 9ab18b6..ed5c858 100644 --- a/packages/react-bootstrap-table2/test/row-selection/wrapper.test.js +++ b/packages/react-bootstrap-table2/test/row-selection/wrapper.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; import Store from '../../src/store/base'; @@ -28,6 +29,8 @@ describe('RowSelectionWrapper', () => { mode: 'radio' }; + const rowIndex = 1; + const keyField = 'id'; const store = new Store({ data, keyField }); @@ -64,10 +67,10 @@ describe('RowSelectionWrapper', () => { const secondSelectedRow = data[1][keyField]; it('call handleRowSelect function should seting correct state.selectedRowKeys', () => { - wrapper.instance().handleRowSelect(firstSelectedRow); + wrapper.instance().handleRowSelect(firstSelectedRow, rowIndex); expect(wrapper.state('selectedRowKeys')).toEqual([firstSelectedRow]); - wrapper.instance().handleRowSelect(secondSelectedRow); + wrapper.instance().handleRowSelect(secondSelectedRow, rowIndex); expect(wrapper.state('selectedRowKeys')).toEqual([secondSelectedRow]); }); }); @@ -90,16 +93,16 @@ describe('RowSelectionWrapper', () => { }); it('call handleRowSelect function should seting correct state.selectedRowKeys', () => { - wrapper.instance().handleRowSelect(firstSelectedRow, true); + wrapper.instance().handleRowSelect(firstSelectedRow, true, rowIndex); expect(wrapper.state('selectedRowKeys')).toEqual(expect.arrayContaining([firstSelectedRow])); - wrapper.instance().handleRowSelect(secondSelectedRow, true); + wrapper.instance().handleRowSelect(secondSelectedRow, true, rowIndex); expect(wrapper.state('selectedRowKeys')).toEqual(expect.arrayContaining([firstSelectedRow, secondSelectedRow])); - wrapper.instance().handleRowSelect(firstSelectedRow, false); + wrapper.instance().handleRowSelect(firstSelectedRow, false, rowIndex); expect(wrapper.state('selectedRowKeys')).toEqual(expect.arrayContaining([secondSelectedRow])); - wrapper.instance().handleRowSelect(secondSelectedRow, false); + wrapper.instance().handleRowSelect(secondSelectedRow, false, rowIndex); expect(wrapper.state('selectedRowKeys')).toEqual([]); }); @@ -119,4 +122,61 @@ describe('RowSelectionWrapper', () => { expect(wrapper.state('selectedRowKeys')).toEqual([]); }); }); + + describe('when selectRow.onSelect is defined', () => { + const selectedRow = data[0][keyField]; + const onSelectCallBack = sinon.stub(); + + beforeEach(() => { + selectRow.mode = 'checkbox'; + selectRow.onSelect = onSelectCallBack; + wrapper = shallow( + + ); + }); + + it('selectRow.onSelect callback should be called correctly when calling handleRowSelect function', () => { + wrapper.instance().handleRowSelect(selectedRow, true, rowIndex); + expect(onSelectCallBack.callCount).toEqual(1); + expect(onSelectCallBack.calledWith(data[0], true, rowIndex)).toBeTruthy(); + + wrapper.instance().handleRowSelect(selectedRow, false, rowIndex); + expect(onSelectCallBack.callCount).toEqual(2); + expect(onSelectCallBack.calledWith(data[0], false, rowIndex)).toBeTruthy(); + }); + }); + + describe('when selectRow.onSelectAll is defined', () => { + const onSelectAllCallBack = sinon.stub(); + + beforeEach(() => { + selectRow.mode = 'checkbox'; + selectRow.onSelectAll = onSelectAllCallBack; + wrapper = shallow( + + ); + }); + + it('selectRow.onSelect callback should be called correctly when calling handleRowSelect function', () => { + wrapper.instance().handleAllRowsSelect(); + expect(onSelectAllCallBack.callCount).toEqual(1); + expect(onSelectAllCallBack.calledWith(true, data)).toBeTruthy(); + + wrapper.instance().handleAllRowsSelect(); + expect(onSelectAllCallBack.callCount).toEqual(2); + expect(onSelectAllCallBack.calledWith(false, [])).toBeTruthy(); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index f720ee7..84241e7 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -605,7 +605,7 @@ describe('Row', () => { }); it('should calling selectRow.onRowSelect with correct argument', () => { - expect(onRowSelectCallBack.calledWith(row[keyField], false)).toBeTruthy(); + expect(onRowSelectCallBack.calledWith(row[keyField], false, rowIndex)).toBeTruthy(); }); }); @@ -636,7 +636,7 @@ describe('Row', () => { }); it('should calling selectRow.onRowSelect with correct argument', () => { - expect(onRowSelectCallBack.calledWith(row[keyField], true)).toBeTruthy(); + expect(onRowSelectCallBack.calledWith(row[keyField], true, rowIndex)).toBeTruthy(); }); }); }); diff --git a/packages/react-bootstrap-table2/test/store/base.test.js b/packages/react-bootstrap-table2/test/store/base.test.js index 716a350..7bfb8f6 100644 --- a/packages/react-bootstrap-table2/test/store/base.test.js +++ b/packages/react-bootstrap-table2/test/store/base.test.js @@ -87,6 +87,31 @@ describe('Store Base', () => { }); }); + describe('getSelectedRows', () => { + const selected = [1, 4]; + beforeEach(() => { + store.setSelectedRowKeys(selected); + }); + + it('should return all selected rows by store.selected', () => { + const result = store.getSelectedRows(); + expect(result).toBeDefined(); + expect(result.length).toEqual(2); + result.forEach((r) => { + expect(r).toBeDefined(); + expect(selected.includes(r[store.keyField])).toBeTruthy(); + }); + }); + }); + + describe('setSelectedRowKeys', () => { + const selected = [1, 4]; + it('should set store.selected correctly', () => { + store.setSelectedRowKeys(selected); + expect(store.getSelectedRowKeys()).toEqual(selected); + }); + }); + describe('edit', () => { it('should update a specified field correctly', () => { const newValue = 'newValue';