diff --git a/docs/README.md b/docs/README.md
index 3150a7b..66e6923 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,6 +14,7 @@
* [hover](#hover)
* [condensed](#condensed)
* [cellEdit](#cellEdit)
+* [selectRow](#selectRow)
### keyField(**required**) - [String]
`keyField` is a prop to tell `react-bootstrap-table2` which column is unigue key.
@@ -36,7 +37,7 @@ Same as `.table-hover` class for adding a hover effect (grey background color) o
### condensed - [Bool]
Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half
-### cellEdit - [Bool]
+### cellEdit - [Object]
Assign a valid `cellEdit` object can enable the cell editing on the cell. The default usage is click/dbclick to trigger cell editing and press `ENTER` to save cell or press `ESC` to cancel editing.
> Note: The `keyField` column can't be edited
@@ -63,4 +64,7 @@ Default is `false`, enable it will be able to save the cell automatically when b
`cellEdit.nonEditableRows` accept a callback function and expect return an array which used to restrict all the columns of some rows as non-editable. So the each item in return array should be rowkey(`keyField`)
#### cellEdit.timeToCloseMessage - [Function]
-If a [`column.validator`](./columns.md#validator) defined and the new value is invalid, `react-bootstrap-table2` will popup a alert at the bottom of editor. `cellEdit.timeToCloseMessage` is a chance to let you decide how long the alert should be stay. Default is 3000 millisecond.
\ No newline at end of file
+If a [`column.validator`](./columns.md#validator) defined and the new value is invalid, `react-bootstrap-table2` will popup a alert at the bottom of editor. `cellEdit.timeToCloseMessage` is a chance to let you decide how long the alert should be stay. Default is 3000 millisecond.
+
+### selectRow - [Object]
+Pass prop `selectRow` to enable row selection. For more detail, please navigate to [row selection document](./row-selection.md).
diff --git a/docs/row-selection.md b/docs/row-selection.md
new file mode 100644
index 0000000..3671093
--- /dev/null
+++ b/docs/row-selection.md
@@ -0,0 +1,48 @@
+
+# Row selection
+`react-bootstrap-table2` supports the row selection feature. By passing prop `selectRow ` to enable row selection. When you enable this feature, `react-bootstrap-table2` will append a new selection column at first.
+
+
+## Available properties
+
+The following are available properties in `selectRow`:
+
+#### Required
+* [mode (required)](#mode)
+
+#### Optional
+
+## selectRow.mode - [String]
+
+Specifying the selection way for `single(radio)` or `multiple(checkbox)`. If `radio` was assigned, there will be a radio button in the selection column; otherwise, the `checkbox` instead.
+
+#### values
+* `radio`
+* `checkbox`
+
+#### examples
+
+```js
+const selectRowProp = {
+ mode: 'radio' // single row selection
+};
+
+```
+
+```js
+const selectRowProp = {
+ mode: 'checkbox' // multiple row selection
+};
+
+
+```
diff --git a/packages/react-bootstrap-table2-example/examples/row-selection/multiple-selection.js b/packages/react-bootstrap-table2-example/examples/row-selection/multiple-selection.js
new file mode 100644
index 0000000..46f2453
--- /dev/null
+++ b/packages/react-bootstrap-table2-example/examples/row-selection/multiple-selection.js
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import { BootstrapTableful } 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 selectRowProp = {
+ mode: 'checkbox'
+};
+
+const sourceCode = `\
+const columns = [{
+ dataField: 'id',
+ text: 'Product ID'
+}, {
+ dataField: 'name',
+ text: 'Product Name'
+}, {
+ dataField: 'price',
+ text: 'Product Price'
+}];
+
+const selectRowProp = {
+ mode: 'checkbox'
+};
+
+
+`;
+
+export default () => (
+
+
+ { sourceCode }
+
+);
diff --git a/packages/react-bootstrap-table2-example/examples/row-selection/single-selection.js b/packages/react-bootstrap-table2-example/examples/row-selection/single-selection.js
new file mode 100644
index 0000000..b02f058
--- /dev/null
+++ b/packages/react-bootstrap-table2-example/examples/row-selection/single-selection.js
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import { BootstrapTableful } 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 selectRowProp = {
+ mode: 'radio'
+};
+
+const sourceCode = `\
+const columns = [{
+ dataField: 'id',
+ text: 'Product ID'
+}, {
+ dataField: 'name',
+ text: 'Product Name'
+}, {
+ dataField: 'price',
+ text: 'Product Price'
+}];
+
+const selectRowProp = {
+ mode: 'radio'
+};
+
+
+`;
+
+export default () => (
+
+
+ { sourceCode }
+
+);
diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js
index 62829ef..4f0d85f 100644
--- a/packages/react-bootstrap-table2-example/stories/index.js
+++ b/packages/react-bootstrap-table2-example/stories/index.js
@@ -46,6 +46,10 @@ import CellLevelEditable from 'examples/cell-edit/cell-level-editable-table';
import CellEditHooks from 'examples/cell-edit/cell-edit-hooks-table';
import CellEditValidator from 'examples/cell-edit/cell-edit-validator-table';
+// work on row selection
+import SingleSelectionTable from 'examples/row-selection/single-selection';
+import MultipleSelectionTable from 'examples/row-selection/multiple-selection';
+
// css style
import 'bootstrap/dist/css/bootstrap.min.css';
import 'stories/stylesheet/tomorrow.min.css';
@@ -99,3 +103,7 @@ storiesOf('Cell Editing', module)
.add('Cell Level Editable', () => )
.add('Rich Hook Functions', () => )
.add('Validation', () => );
+
+storiesOf('Row Selection', module)
+ .add('Single selection', () => )
+ .add('Multiple selection', () => );
diff --git a/packages/react-bootstrap-table2/src/body.js b/packages/react-bootstrap-table2/src/body.js
index 76cbd22..9a0cbba 100644
--- a/packages/react-bootstrap-table2/src/body.js
+++ b/packages/react-bootstrap-table2/src/body.js
@@ -1,10 +1,13 @@
/* eslint react/prop-types: 0 */
+/* eslint react/require-default-props: 0 */
+
import React from 'react';
import PropTypes from 'prop-types';
import _ from './utils';
import Row from './row';
import RowSection from './row-section';
+import Const from './const';
const Body = (props) => {
const {
@@ -14,7 +17,9 @@ const Body = (props) => {
isEmpty,
noDataIndication,
visibleColumnSize,
- cellEdit
+ cellEdit,
+ selectRow,
+ selectedRowKeys
} = props;
let content;
@@ -25,7 +30,13 @@ const Body = (props) => {
} else {
content = data.map((row, index) => {
const key = _.get(row, keyField);
- const editable = !(cellEdit && cellEdit.nonEditableRows.indexOf(key) > -1);
+ const editable = !(cellEdit.mode !== Const.UNABLE_TO_CELL_EDIT &&
+ cellEdit.nonEditableRows.indexOf(key) > -1);
+
+ const selected = selectRow.mode !== Const.ROW_SELECT_DISABLED
+ ? selectedRowKeys.includes(key)
+ : null;
+
return (
{
columns={ columns }
cellEdit={ cellEdit }
editable={ editable }
+ selected={ selected }
+ selectRow={ selectRow }
/>
);
});
@@ -48,7 +61,9 @@ const Body = (props) => {
Body.propTypes = {
keyField: PropTypes.string.isRequired,
data: PropTypes.array.isRequired,
- columns: PropTypes.array.isRequired
+ columns: PropTypes.array.isRequired,
+ selectRow: PropTypes.object,
+ selectedRowKeys: PropTypes.array
};
export default Body;
diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js
index f4aaf8b..2aaf510 100644
--- a/packages/react-bootstrap-table2/src/bootstrap-table.js
+++ b/packages/react-bootstrap-table2/src/bootstrap-table.js
@@ -22,8 +22,11 @@ class BootstrapTable extends PropsBaseResolver(Component) {
this.startEditing = this.startEditing.bind(this);
this.escapeEditing = this.escapeEditing.bind(this);
this.completeEditing = this.completeEditing.bind(this);
+ this.handleRowSelect = this.handleRowSelect.bind(this);
+ this.handleAllRowsSelect = this.handleAllRowsSelect.bind(this);
this.state = {
data: this.store.get(),
+ selectedRowKeys: this.store.getSelectedRowKeys(),
currEditCell: {
ridx: null,
cidx: null
@@ -56,6 +59,14 @@ class BootstrapTable extends PropsBaseResolver(Component) {
onComplete: this.completeEditing
});
+ const cellSelectionInfo = this.resolveCellSelectionProps({
+ onRowSelect: this.handleRowSelect
+ });
+
+ const headerCellSelectionInfo = this.resolveHeaderCellSelectionProps({
+ onAllRowsSelect: this.handleAllRowsSelect
+ });
+
return (
@@ -65,6 +76,7 @@ class BootstrapTable extends PropsBaseResolver(Component) {
sortField={ this.store.sortField }
sortOrder={ this.store.sortOrder }
onSort={ this.handleSort }
+ selectRow={ headerCellSelectionInfo }
/>
);
}
+ /**
+ * row selection handler
+ * @param {String} rowKey - row key of what was selected.
+ * @param {Boolean} checked - next checked status of input button.
+ */
+ handleRowSelect(rowKey, checked) {
+ const { mode } = this.props.selectRow;
+ const { ROW_SELECT_SINGLE } = Const;
+
+ let currSelected = [...this.store.getSelectedRowKeys()];
+
+ if (mode === ROW_SELECT_SINGLE) { // when select mode is radio
+ currSelected = [rowKey];
+ } else if (checked) { // when select mode is checkbox
+ currSelected.push(rowKey);
+ } else {
+ currSelected = currSelected.filter(value => value !== rowKey);
+ }
+
+ this.store.setSelectedRowKeys(currSelected);
+
+ this.setState(() => ({
+ selectedRowKeys: currSelected
+ }));
+ }
+
+ /**
+ * handle all rows selection on header cell by store.selected or given specific result.
+ * @param {Boolean} option - customized result for all rows selection
+ */
+ handleAllRowsSelect(option) {
+ const selected = this.store.isAnySelectedRow();
+
+ // set next status of all row selected by store.selected or customizing by user.
+ const result = option || !selected;
+
+ const currSelected = result ? this.store.selectAllRowKeys() : [];
+
+ this.store.setSelectedRowKeys(currSelected);
+
+ this.setState(() => ({
+ selectedRowKeys: currSelected
+ }));
+ }
+
handleSort(column) {
this.store.sortBy(column);
@@ -146,6 +205,9 @@ BootstrapTable.propTypes = {
afterSaveCell: PropTypes.func,
nonEditableRows: PropTypes.func,
timeToCloseMessage: PropTypes.number
+ }),
+ selectRow: PropTypes.shape({
+ mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired
})
};
diff --git a/packages/react-bootstrap-table2/src/const.js b/packages/react-bootstrap-table2/src/const.js
index 5391b82..4e10028 100644
--- a/packages/react-bootstrap-table2/src/const.js
+++ b/packages/react-bootstrap-table2/src/const.js
@@ -4,5 +4,11 @@ export default {
UNABLE_TO_CELL_EDIT: 'none',
CLICK_TO_CELL_EDIT: 'click',
DBCLICK_TO_CELL_EDIT: 'dbclick',
- TIME_TO_CLOSE_MESSAGE: 3000
+ TIME_TO_CLOSE_MESSAGE: 3000,
+ ROW_SELECT_SINGLE: 'radio',
+ ROW_SELECT_MULTIPLE: 'checkbox',
+ ROW_SELECT_DISABLED: 'ROW_SELECT_DISABLED',
+ CHECKBOX_STATUS_CHECKED: 'checked',
+ CHECKBOX_STATUS_INDETERMINATE: 'indeterminate',
+ CHECKBOX_STATUS_UNCHECKED: 'unchecked'
};
diff --git a/packages/react-bootstrap-table2/src/header.js b/packages/react-bootstrap-table2/src/header.js
index 36752f1..8953c63 100644
--- a/packages/react-bootstrap-table2/src/header.js
+++ b/packages/react-bootstrap-table2/src/header.js
@@ -1,20 +1,28 @@
/* eslint react/require-default-props: 0 */
import React from 'react';
import PropTypes from 'prop-types';
+import Const from './const';
import HeaderCell from './header-cell';
-
+import SelectionHeaderCell from './row-selection/selection-header-cell';
const Header = (props) => {
+ const { ROW_SELECT_DISABLED } = Const;
+
const {
columns,
onSort,
sortField,
- sortOrder
+ sortOrder,
+ selectRow
} = props;
+
return (
+ {
+ selectRow.mode === ROW_SELECT_DISABLED ? null :
+ }
{
columns.map((column, i) => {
const currSort = column.dataField === sortField;
@@ -38,7 +46,8 @@ Header.propTypes = {
columns: PropTypes.array.isRequired,
onSort: PropTypes.func,
sortField: PropTypes.string,
- sortOrder: PropTypes.string
+ sortOrder: PropTypes.string,
+ selectRow: PropTypes.object
};
export default Header;
diff --git a/packages/react-bootstrap-table2/src/props-resolver/index.js b/packages/react-bootstrap-table2/src/props-resolver/index.js
index d7ea8af..f01790c 100644
--- a/packages/react-bootstrap-table2/src/props-resolver/index.js
+++ b/packages/react-bootstrap-table2/src/props-resolver/index.js
@@ -40,4 +40,65 @@ export default ExtendBase =>
...cellEditInfo
};
}
+
+ /**
+ * props resolver for cell selection
+ * @param {Object} options - addtional options like callback which are about to merge into props
+ *
+ * @returns {Object} result - props for cell selections
+ * @returns {String} result.mode - input type of row selection or disabled.
+ */
+ resolveCellSelectionProps(options) {
+ const { selectRow } = this.props;
+ const { ROW_SELECT_DISABLED } = Const;
+
+ if (_.isDefined(selectRow)) {
+ return {
+ ...selectRow,
+ ...options
+ };
+ }
+
+ return {
+ mode: ROW_SELECT_DISABLED
+ };
+ }
+
+ /**
+ * props resolver for header cell selection
+ * @param {Object} options - addtional options like callback which are about to merge into props
+ *
+ * @returns {Object} result - props for cell selections
+ * @returns {String} result.mode - input type of row selection or disabled.
+ * @returns {String} result.checkedStatus - checkbox status depending on selected rows counts
+ */
+ resolveHeaderCellSelectionProps(options) {
+ const { selected } = this.store;
+ const { selectRow } = this.props;
+ const {
+ ROW_SELECT_DISABLED, CHECKBOX_STATUS_CHECKED,
+ CHECKBOX_STATUS_INDETERMINATE, CHECKBOX_STATUS_UNCHECKED
+ } = Const;
+
+ if (_.isDefined(selectRow)) {
+ let checkedStatus;
+
+ const allRowsSelected = this.store.isAllRowsSelected();
+
+ // checkbox status depending on selected rows counts
+ if (allRowsSelected) checkedStatus = CHECKBOX_STATUS_CHECKED;
+ else if (selected.length === 0) checkedStatus = CHECKBOX_STATUS_UNCHECKED;
+ else checkedStatus = CHECKBOX_STATUS_INDETERMINATE;
+
+ return {
+ ...selectRow,
+ ...options,
+ checkedStatus
+ };
+ }
+
+ return {
+ mode: ROW_SELECT_DISABLED
+ };
+ }
};
diff --git a/packages/react-bootstrap-table2/src/row-selection/selection-cell.js b/packages/react-bootstrap-table2/src/row-selection/selection-cell.js
new file mode 100644
index 0000000..73b3e4a
--- /dev/null
+++ b/packages/react-bootstrap-table2/src/row-selection/selection-cell.js
@@ -0,0 +1,59 @@
+/* eslint
+ react/require-default-props: 0
+ jsx-a11y/no-noninteractive-element-interactions: 0
+*/
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Const from '../const';
+
+export default class SelectionCell extends Component {
+ static propTypes = {
+ mode: PropTypes.string.isRequired,
+ rowKey: PropTypes.any,
+ selected: PropTypes.bool,
+ onRowSelect: PropTypes.func
+ }
+
+ constructor() {
+ super();
+ this.handleRowClick = this.handleRowClick.bind(this);
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { selected } = this.props;
+
+ return nextProps.selected !== selected;
+ }
+
+ handleRowClick() {
+ const { ROW_SELECT_SINGLE } = Const;
+ const {
+ mode: inputType,
+ rowKey,
+ selected,
+ onRowSelect
+ } = this.props;
+
+ const checked = inputType === ROW_SELECT_SINGLE
+ ? true
+ : !selected;
+
+ onRowSelect(rowKey, checked);
+ }
+
+ render() {
+ const {
+ mode: inputType,
+ selected
+ } = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
diff --git a/packages/react-bootstrap-table2/src/row-selection/selection-header-cell.js b/packages/react-bootstrap-table2/src/row-selection/selection-header-cell.js
new file mode 100644
index 0000000..08516d1
--- /dev/null
+++ b/packages/react-bootstrap-table2/src/row-selection/selection-header-cell.js
@@ -0,0 +1,76 @@
+/* eslint react/require-default-props: 0 */
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Constant from '../const';
+
+export const CheckBox = ({ checked, indeterminate }) => (
+ {
+ if (input) input.indeterminate = indeterminate; // eslint-disable-line no-param-reassign
+ }}
+ />
+);
+
+CheckBox.propTypes = {
+ checked: PropTypes.bool.isRequired,
+ indeterminate: PropTypes.bool.isRequired
+};
+
+export default class SelectionHeaderCell extends Component {
+ static propTypes = {
+ mode: PropTypes.string.isRequired,
+ checkedStatus: PropTypes.string,
+ onAllRowsSelect: PropTypes.func
+ }
+
+ constructor() {
+ super();
+ this.handleCheckBoxClick = this.handleCheckBoxClick.bind(this);
+ }
+
+ /**
+ * avoid updating if button is
+ * 1. radio
+ * 2. status was not changed.
+ */
+ shouldComponentUpdate(nextProps) {
+ const { ROW_SELECT_SINGLE } = Constant;
+ const { mode, checkedStatus } = this.props;
+
+ if (mode === ROW_SELECT_SINGLE) return false;
+
+ return nextProps.checkedStatus !== checkedStatus;
+ }
+
+ handleCheckBoxClick() {
+ const { onAllRowsSelect } = this.props;
+
+ onAllRowsSelect();
+ }
+
+ render() {
+ const {
+ CHECKBOX_STATUS_CHECKED, CHECKBOX_STATUS_INDETERMINATE, ROW_SELECT_SINGLE
+ } = Constant;
+
+ const { mode, checkedStatus } = this.props;
+
+ const checked = checkedStatus === CHECKBOX_STATUS_CHECKED;
+
+ const indeterminate = checkedStatus === CHECKBOX_STATUS_INDETERMINATE;
+
+ return mode === ROW_SELECT_SINGLE
+ ?
+ : (
+
+
+
+ );
+ }
+}
diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js
index 117dce4..09d9d7b 100644
--- a/packages/react-bootstrap-table2/src/row.js
+++ b/packages/react-bootstrap-table2/src/row.js
@@ -4,17 +4,24 @@ import PropTypes from 'prop-types';
import _ from './utils';
import Cell from './cell';
+import SelectionCell from './row-selection/selection-cell';
import EditingCell from './editing-cell';
+import Const from './const';
const Row = (props) => {
+ const { ROW_SELECT_DISABLED } = Const;
+
const {
row,
columns,
keyField,
rowIndex,
cellEdit,
+ selected,
+ selectRow,
editable: editableRow
} = props;
+
const {
mode,
onStart,
@@ -22,8 +29,20 @@ const Row = (props) => {
cidx: editingColIdx,
...rest
} = cellEdit;
+
return (
+ {
+ selectRow.mode === ROW_SELECT_DISABLED
+ ? null
+ : (
+
+ )
+ }
{
columns.map((column, index) => {
const { dataField } = column;
diff --git a/packages/react-bootstrap-table2/src/store/base.js b/packages/react-bootstrap-table2/src/store/base.js
index 2c92725..31a96cd 100644
--- a/packages/react-bootstrap-table2/src/store/base.js
+++ b/packages/react-bootstrap-table2/src/store/base.js
@@ -10,6 +10,7 @@ export default class Store {
this.sortOrder = undefined;
this.sortField = undefined;
+ this.selected = [];
}
isEmpty() {
@@ -39,4 +40,24 @@ export default class Store {
getRowByRowId(rowId) {
return this.get().find(row => _.get(row, this.keyField) === rowId);
}
+
+ setSelectedRowKeys(selectedKeys) {
+ this.selected = selectedKeys;
+ }
+
+ getSelectedRowKeys() {
+ return this.selected;
+ }
+
+ selectAllRowKeys() {
+ return this.data.map(row => _.get(row, this.keyField));
+ }
+
+ isAllRowsSelected() {
+ return this.data.length === this.selected.length;
+ }
+
+ isAnySelectedRow() {
+ return this.selected.length > 0;
+ }
}
diff --git a/packages/react-bootstrap-table2/style/react-bootstrap-table.scss b/packages/react-bootstrap-table2/style/react-bootstrap-table.scss
index 9720bd0..1a87c51 100644
--- a/packages/react-bootstrap-table2/style/react-bootstrap-table.scss
+++ b/packages/react-bootstrap-table2/style/react-bootstrap-table.scss
@@ -22,6 +22,10 @@
margin: 10px 6.5px;
}
+ th[data-row-selection] {
+ width: 30px;
+ }
+
td.react-bs-table-no-data {
text-align: center;
}
diff --git a/packages/react-bootstrap-table2/test/body.test.js b/packages/react-bootstrap-table2/test/body.test.js
index 54ba506..30f01dd 100644
--- a/packages/react-bootstrap-table2/test/body.test.js
+++ b/packages/react-bootstrap-table2/test/body.test.js
@@ -6,6 +6,7 @@ import Body from '../src/body';
import Row from '../src/row';
import Const from '../src/const';
import RowSection from '../src/row-section';
+import mockBodyResolvedProps from '../test/mock-data/body-resolved-props';
describe('Body', () => {
let wrapper;
@@ -27,7 +28,7 @@ describe('Body', () => {
describe('simplest body', () => {
beforeEach(() => {
- wrapper = shallow( );
+ wrapper = shallow( );
});
it('should render successfully', () => {
@@ -41,6 +42,7 @@ describe('Body', () => {
beforeEach(() => {
wrapper = shallow(
{
emptyIndication = 'Table is empty';
wrapper = shallow(
{
emptyIndicationCallBack = sinon.stub().returns(content);
wrapper = shallow(
{
beforeEach(() => {
wrapper = shallow(
{
}
});
});
+
+ describe('when selectRow.mode is checkbox or radio (row was selectable)', () => {
+ const keyField = 'id';
+ const selectRow = { mode: 'checkbox' };
+
+ it('props selected should be true if all rows were selected', () => {
+ wrapper = shallow(
+
+ );
+
+ expect(wrapper.find(Row).get(0).props.selected).toBe(true);
+ });
+
+ it('props selected should be false if all rows were not selected', () => {
+ wrapper = shallow(
+