* support async cell editing

* refine cellEdit.onUpdate and cellEdit.editing

* refine cell edit

* add redux

* add stories for async cell edit

* fix test case and patch tests for async cell editing

* patch docs for cellEdit prop

* fix bug produced by rebasing lol
This commit is contained in:
Allen
2017-10-17 02:27:48 -05:00
committed by GitHub
parent 877259158e
commit 7a5b88bcf6
22 changed files with 1232 additions and 199 deletions
+1 -30
View File
@@ -38,33 +38,4 @@ Same as `.table-hover` class for adding a hover effect (grey background color) o
Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half
### <a name='cellEdit'>cellEdit - [Object]</a>
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
Following is a `cellEdit` object:
```js
{
mode: 'click',
blurToSave: true,
timeToCloseMessage: 2500,
onEditing: (rowId, dataField, newValue) => { ... },
beforeSaveCell: (oldValue, newValue, row, column) => { ... },
afterSaveCell: (oldValue, newValue, row, column) => { ... },
nonEditableRows: () => { ... }
}
```
#### <a name='cellEdit.mode'>cellEdit.mode - [String]</a>
`cellEdit.mode` possible value is `click` and `dbclick`. **It's required value** that tell `react-bootstrap-table2` how to trigger the cell editing.
#### <a name='cellEdit.blurToSave'>cellEdit.blurToSave - [Bool]</a>
Default is `false`, enable it will be able to save the cell automatically when blur from the cell editor.
#### <a name='cellEdit.nonEditableRows'>cellEdit.nonEditableRows - [Function]</a>
`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`)
#### <a name='cellEdit.timeToCloseMessage'>cellEdit.timeToCloseMessage - [Function]</a>
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.
### <a name='selectRow'>selectRow - [Object]</a>
Pass prop `selectRow` to enable row selection. For more detail, please navigate to [row selection document](./row-selection.md).
`cellEdit` props accept an object, please see [cellEdit definition](./cell-edit-prop.md) for more detail.
+136
View File
@@ -0,0 +1,136 @@
# Properties on cellEdit prop
* [mode (**required**)](#mode)
* [blurToSave](#blurToSave)
* [nonEditableRows](#nonEditableRows)
* [timeToCloseMessage](#timeToCloseMessage)
* [beforeSaveCell](#beforeSaveCell)
* [afterSaveCell](#afterSaveCell)
* [onUpdate](#onUpdate)
* [editing](#editing)
* [errorMessage](#errorMessage)
* [onErrorMessageDisappear](#onErrorMessageDisappear)
## <a name='cellEdit'>cellEdit - [Object]</a>
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
Following is the shape of `cellEdit` object:
```js
{
mode: 'click',
blurToSave: true,
timeToCloseMessage: 2500,
editing: false|true,
errorMessage: '',
onUpdate: (rowId, dataField, newValue) => { ... },
beforeSaveCell: (oldValue, newValue, row, column) => { ... },
afterSaveCell: (oldValue, newValue, row, column) => { ... },
onErrorMessageDisappear: () => { ... },
nonEditableRows: () => { ... }
}
```
### <a name='mode'>cellEdit.mode - [String]</a>
`cellEdit.mode` possible value is `click` and `dbclick`. **It's required value** that tell `react-bootstrap-table2` how to trigger the cell editing.
### <a name='blurToSave'>cellEdit.blurToSave - [Bool]</a>
Default is `false`, enable it will be able to save the cell automatically when blur from the cell editor.
### <a name='nonEditableRows'>cellEdit.nonEditableRows - [Function]</a>
`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`)
### <a name='timeToCloseMessage'>cellEdit.timeToCloseMessage - [Function]</a>
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.
### <a name='beforeSaveCell'>cellEdit.beforeSaveCell - [Function]</a>
This callback function will be called before triggering cell update.
```js
const cellEdit = {
// omit...
beforeSaveCell: (oldValue, newValue, row, column) => { ... }
}
```
### <a name='afterSaveCell'>cellEdit.afterSaveCell - [Function]</a>
This callback function will be called after updating cell.
```js
const cellEdit = {
// omit...
afterSaveCell: (oldValue, newValue, row, column) => { ... }
};
```
### <a name='onUpdate'>cellEdit.onUpdate - [Function]</a>
If you want to control the cell updating process by yourself, for example, connect with `Redux` or saving data to backend database, `cellEdit.onUpdate` is a great chance you can work on it.
Firsylt, `react-bootstrap-table2` allow `cellEdit.onUpdate` to return a promise:
```js
const cellEdit = {
// omit...
onUpdate: (rowId, dataField, newValue) => {
return apiCall().then(response => {
console.log('update cell to backend successfully');
// Actually, you dont do any thing here, we will update the new value when resolve your promise
})
.catch(err => throw new Error(err.message));
}
};
```
If your promise is resolved successfully, `react-bootstrap-table2` will default help you to update the new cell value.
If your promise is resolved failure, you can throw an `Error` instance, `react-bootstrap-table2` will show up the error message (Same behavior like [`column.validator`](./columns.md#validator)).
In some case, backend will return a new value to client side and you want to apply this new value instead of the value that user input. In this situation, you can return an object which contain a `value` property:
```js
const cellEdit = {
// omit...
onUpdate: (rowId, dataField, newValue) => {
return apiCall().then(response => {
return { value: response.value }; // response.value is from your backend api
})
.catch(err => throw new Error(err.message));
}
};
```
If your application integgrate with `Redux`, you may need to dispatch an action in `cellEdit.onUpdate` callback. In this circumstances, you need to return `false` explicity which `react-bootstrap-table2` will stop any operation internally and wait rerender by your application.
In a simple redux application, you probably need to handle those props by your application:
* [`cellEdit.editing`](#editing): Is cell still on editing or not? This value should always be `true` when saving cell failure.
* [`cellEdit.errorMessage`](#errorMessage): Error message when save the cell failure.
* [`cellEdit.onErrorMessageDisappear`](#onErrorMessageDisappear): This callback will be called when error message alert closed automatically.
* `cellEdit.onUpdate`
```js
const cellEdit = {
editing: this.props.editing,
errorMessage: this.props.errorMessage,
onErrorMessageDisappear: () => {
// cleanErrorMessage is an action creator
this.props.dispatch(cleanErrorMessage());
},
onUpdate: (rowId, dataField, newValue) => {
// updateCell is an action creator
this.props.dispatch(updateCell(rowId, dataField, newValue)));
return false; // Have to return false here
}
};
```
Please check [this](https://github.com/react-bootstrap-table/react-bootstrap-table2/blob/develop/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js) exmaple to learn how use `cellEdit` with a redux application
### <a name='editing'>cellEdit.editing - [Bool]</a>
This only used when you want to control cell saving externally, `cellEdit.editing` will be a flag to tell `react-bootstrap-table2` whether currecnt editing cell is still editing or not. Because, it's possible that some error happen when you saving cell, in this situation, you need to configre this value as `false` to keep the cell as edtiable and show up an error message.
### <a name='errorMessage'>cellEdit.errorMessage - [String]</a>
Same as [`cellEdit.editing`](#editing). This prop is not often used. Only used when you keep the error message in your application state.
### <a name='onErrorMessageDisappear'>cellEdit.onErrorMessageDisappear - [Function]</a>
This callback function will be called when error message discard.
@@ -0,0 +1,75 @@
/* eslint no-unused-vars: 0 */
/* eslint arrow-body-style: 0 */
import React, { Component } from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator, sleep } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
class CellEditWithPromise extends Component {
handleCellEditing = (rowId, dataField, newValue) => {
return sleep(1000).then(() => {
if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) {
throw new Error('Product Price should bigger than $2000');
}
});
}
render() {
const cellEdit = {
mode: 'click',
blurToSave: true,
onUpdate: this.handleCellEditing
};
return (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);
}
}
`;
class CellEditWithPromise extends Component {
handleCellEditing = (rowId, dataField, newValue) => {
return sleep(1000).then(() => {
if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) {
throw new Error('Product Price should bigger than $2000');
}
});
}
render() {
const cellEdit = {
mode: 'click',
blurToSave: true,
onUpdate: this.handleCellEditing
};
return (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);
}
}
export default CellEditWithPromise;
@@ -0,0 +1,212 @@
/* eslint no-unused-vars: 0 */
/* eslint react/prop-types: 0 */
/* eslint arrow-body-style: 0 */
/* eslint consistent-return: 0 */
/* eslint no-class-assign: 0 */
import React, { Component } from 'react';
import thunk from 'redux-thunk';
import { Provider, connect } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
/////////////////////// Action Creator ///////////////////////
const setErrorMessage = (errorMessage = null) => {
return { type: 'SET_ERR_MESSAGE', errorMessage };
};
// Async Action, using redux-thunk
const cellEditingAsync = (rowId, dataField, newValue) => {
return (dispatch) => {
setTimeout(() => {
if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) {
dispatch(setErrorMessage('Product Price should bigger than $2000'));
} else {
dispatch({ type: 'ADD_SUCCESS', rowId, dataField, newValue });
}
}, 1200);
};
};
/////////////////////// Component ///////////////////////
class CellEditWithRedux extends Component {
// dispatch a async action
handleCellEditing = (rowId, dataField, newValue) => {
this.props.dispatch(cellEditingAsync(rowId, dataField, newValue));
return false;
}
handleErrorMsgDisappear = () => {
this.props.dispatch(setErrorMessage());
}
render() {
const cellEdit = {
mode: 'click',
editing: this.props.cellEditing,
errorMessage: this.props.errorMessage,
onUpdate: this.handleCellEditing,
onErrorMessageDisappear: this.handleErrorMsgDisappear
};
return (
<div>
<BootstrapTableful keyField="id" data={ this.props.data } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);
}
}
// connect
CellEditWithRedux = connect(state => state)(CellEditWithRedux);
/////////////////////// Reducer ///////////////////////
// initial state object and simple reducers
const initialState = {
data: productsGenerator(),
cellEditing: false,
errorMessage: null
};
const reducers = (state, action) => {
switch (action.type) {
case 'ADD_SUCCESS': {
const { rowId, dataField, newValue } = action;
const data = [...state.data];
const rowIndex = data.findIndex(r => r.id === rowId);
data[rowIndex][dataField] = newValue;
return {
data,
cellEditing: false,
errorMessage: null
};
}
case 'SET_ERR_MESSAGE': {
const { errorMessage } = action;
return {
...state,
cellEditing: true,
errorMessage
};
}
default: {
return { ...state };
}
}
};
/////////////////////// Index ///////////////////////
const store = createStore(reducers, initialState, applyMiddleware(thunk));
const Index = () => (
<Provider store={store}>
<CellEditWithRedux />
</Provider>
);
`;
const setErrorMessage = (errorMessage = null) => {
return { type: 'SET_ERR_MESSAGE', errorMessage };
};
// Async Action, using redux-thunk
const cellEditingAsync = (rowId, dataField, newValue) => {
return (dispatch) => {
setTimeout(() => {
if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) {
dispatch(setErrorMessage('Product Price should bigger than $2000'));
} else {
dispatch({ type: 'ADD_SUCCESS', rowId, dataField, newValue });
}
}, 1200);
};
};
class CellEditWithRedux extends Component {
// dispatch a async action
handleCellEditing = (rowId, dataField, newValue) => {
this.props.dispatch(cellEditingAsync(rowId, dataField, newValue));
return false;
}
handleErrorMsgDisappear = () => {
this.props.dispatch(setErrorMessage());
}
render() {
const cellEdit = {
mode: 'click',
editing: this.props.cellEditing,
errorMessage: this.props.errorMessage,
onUpdate: this.handleCellEditing,
onErrorMessageDisappear: this.handleErrorMsgDisappear
};
return (
<div>
<BootstrapTableful keyField="id" data={ this.props.data } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);
}
}
// connect
CellEditWithRedux = connect(state => state)(CellEditWithRedux);
// initial state object and simple reducers
const initialState = {
data: productsGenerator(),
cellEditing: false,
errorMessage: null
};
const reducers = (state, action) => {
switch (action.type) {
case 'ADD_SUCCESS': {
const { rowId, dataField, newValue } = action;
const data = JSON.parse(JSON.stringify(state.data));
const rowIndex = data.findIndex(r => r.id === rowId);
data[rowIndex][dataField] = newValue;
return {
data,
cellEditing: false,
errorMessage: null
};
}
case 'SET_ERR_MESSAGE': {
const { errorMessage } = action;
return {
...state,
cellEditing: true,
errorMessage
};
}
default: {
return { ...state };
}
}
};
const store = createStore(reducers, initialState, applyMiddleware(thunk));
const Index = () => (
<Provider store={store}>
<CellEditWithRedux />
</Provider>
);
export default Index;
@@ -21,6 +21,9 @@
},
"devDependencies": {
"@storybook/react": "^3.2.8",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"typed.js": "^2.0.5"
}
}
+68 -16
View File
@@ -1,7 +1,12 @@
/* eslint no-unused-vars: 0 */
/* eslint no-debugger: 0 */
/* eslint arrow-body-style: 0 */
import React from 'react';
import ReactDom from 'react-dom';
import { BootstrapTableful } from 'react-bootstrap-table2';
import { BootstrapTableful, createTable } from 'react-bootstrap-table2';
require('react-bootstrap-table2/style/react-bootstrap-table.scss');
const products = [];
@@ -29,38 +34,85 @@ const columns = [{
style: {
backgroundColor: 'red'
},
headerTitle: (column, colIndex) => {
console.log(column);
console.log(colIndex);
return 'yes~~~ oh';
},
classes: 'my-xxx'
}, {
dataField: 'name',
text: 'Product Name',
headerTitle: true,
formatter: (cell, row) =>
(<h3>{ cell }::: ${ row.price }</h3>)
(<h3>{ cell }::: ${ row.price }</h3>),
validator: (newValue, row, column) => {
const validationForm = {
valid: true,
message: null
};
validationForm.valid = false;
validationForm.message = 'Invalid message';
return validationForm;
}
}, {
dataField: 'price',
text: 'Product Price',
style: (cell, row, colIndex) => {
console.log(cell);
console.log(row);
console.log(colIndex);
return {
backgroundColor: 'blue'
};
validator: (newValue, row, column) => {
if (newValue < 2000) {
return {
valid: false,
message: 'Price should bigger than 2000'
};
}
return true;
}
}, {
dataField: 'nest.address',
text: 'Address'
}, {
dataField: 'nest.postcal',
text: 'Postal'
text: 'Postal',
editable: true,
validator: (newValue, row, column) => true
}];
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const cellEdit = {
mode: 'click',
blurToSave: true,
// beforeSaveCell: (oldValue, newValue, row, column) => {
// console.log('beforeSavecell');
// // console.log(oldValue);
// // console.log(newValue);
// // console.log(row);
// // console.log(column);
// },
// afterSaveCell: (oldValue, newValue, row, column) => {
// console.log('aftersavecell');
// // console.log(oldValue);
// // console.log(newValue);
// // console.log(row);
// // console.log(column);
// }
onUpdate: (rowId, dataField, newValue) => {
return sleep(1000).then(() => {
// return { forceUpdate: true };
throw new Error('test is not successfully');
});
}
// nonEditableRows: () => [1, 3, 7]
};
// let Table = withCellEdit({
// mode: 'click',
// blurToSave: true,
// onEditing: (rowId, dataField, newValue) => {
// return sleep(1000).then(() => {
// // return { forceUpdate: true };
// throw new Error('test is not successfully');
// });
// }
// })(BootstrapTable);
// Table = createTable(Table);
ReactDom.render(
<BootstrapTableful keyField="id" data={ products } columns={ columns } />,
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />,
document.getElementById('example'));
@@ -19,3 +19,5 @@ export const productsGenerator = (quantity = 5, callback) => {
}))
);
};
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+6 -1
View File
@@ -45,6 +45,8 @@ import ColumnLevelEditableTable from 'examples/cell-edit/column-level-editable-t
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';
import CellEditWithPromise from 'examples/cell-edit/cell-edit-with-promise-table';
import CellEditWithRedux from 'examples/cell-edit/cell-edit-with-redux-table';
// work on row selection
import SingleSelectionTable from 'examples/row-selection/single-selection';
@@ -102,8 +104,11 @@ storiesOf('Cell Editing', module)
.add('Column Level Editable', () => <ColumnLevelEditableTable />)
.add('Cell Level Editable', () => <CellLevelEditable />)
.add('Rich Hook Functions', () => <CellEditHooks />)
.add('Validation', () => <CellEditValidator />);
.add('Validation', () => <CellEditValidator />)
.add('Async Cell Editing(Promise)', () => <CellEditWithPromise />)
.add('Async Cell Editing(Redux)', () => <CellEditWithRedux />);
storiesOf('Row Selection', module)
.add('Single selection', () => <SingleSelectionTable />)
.add('Multiple selection', () => <MultipleSelectionTable />);
+42 -62
View File
@@ -6,36 +6,32 @@ import cs from 'classnames';
import Header from './header';
import Caption from './caption';
import Body from './body';
import Store from './store/base';
import PropsBaseResolver from './props-resolver';
import Const from './const';
import _ from './utils';
class BootstrapTable extends PropsBaseResolver(Component) {
constructor(props) {
super(props);
this.validateProps();
const { store } = this.props;
this.store = !store ? new Store(props) : store;
this.handleSort = this.handleSort.bind(this);
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
}
data: props.store.get(),
selectedRowKeys: props.store.getSelectedRowKeys()
};
}
componentWillReceiveProps(nextProps) {
this.setState({
data: nextProps.store.get()
});
}
render() {
const {
store,
columns,
keyField,
striped,
@@ -54,9 +50,10 @@ class BootstrapTable extends PropsBaseResolver(Component) {
});
const cellEditInfo = this.resolveCellEditProps({
onStart: this.startEditing,
onEscape: this.escapeEditing,
onComplete: this.completeEditing
onStart: this.props.onStartEditing,
onEscape: this.props.onEscapeEditing,
onUpdate: this.props.onCellUpdate,
currEditCell: this.props.currEditCell
});
const cellSelectionInfo = this.resolveCellSelectionProps({
@@ -64,7 +61,9 @@ class BootstrapTable extends PropsBaseResolver(Component) {
});
const headerCellSelectionInfo = this.resolveHeaderCellSelectionProps({
onAllRowsSelect: this.handleAllRowsSelect
onAllRowsSelect: this.handleAllRowsSelect,
selected: store.selected,
allRowsSelected: store.isAllRowsSelected()
});
return (
@@ -73,8 +72,8 @@ class BootstrapTable extends PropsBaseResolver(Component) {
<Caption>{ caption }</Caption>
<Header
columns={ columns }
sortField={ this.store.sortField }
sortOrder={ this.store.sortOrder }
sortField={ store.sortField }
sortOrder={ store.sortOrder }
onSort={ this.handleSort }
selectRow={ headerCellSelectionInfo }
/>
@@ -100,10 +99,10 @@ class BootstrapTable extends PropsBaseResolver(Component) {
* @param {Boolean} checked - next checked status of input button.
*/
handleRowSelect(rowKey, checked) {
const { mode } = this.props.selectRow;
const { selectRow: { mode }, store } = this.props;
const { ROW_SELECT_SINGLE } = Const;
let currSelected = [...this.store.getSelectedRowKeys()];
let currSelected = [...store.getSelectedRowKeys()];
if (mode === ROW_SELECT_SINGLE) { // when select mode is radio
currSelected = [rowKey];
@@ -113,7 +112,7 @@ class BootstrapTable extends PropsBaseResolver(Component) {
currSelected = currSelected.filter(value => value !== rowKey);
}
this.store.setSelectedRowKeys(currSelected);
store.setSelectedRowKeys(currSelected);
this.setState(() => ({
selectedRowKeys: currSelected
@@ -125,14 +124,15 @@ class BootstrapTable extends PropsBaseResolver(Component) {
* @param {Boolean} option - customized result for all rows selection
*/
handleAllRowsSelect(option) {
const selected = this.store.isAnySelectedRow();
const { store } = this.props;
const selected = 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() : [];
const currSelected = result ? store.selectAllRowKeys() : [];
this.store.setSelectedRowKeys(currSelected);
store.setSelectedRowKeys(currSelected);
this.setState(() => ({
selectedRowKeys: currSelected
@@ -140,44 +140,12 @@ class BootstrapTable extends PropsBaseResolver(Component) {
}
handleSort(column) {
this.store.sortBy(column);
const { store } = this.props;
store.sortBy(column);
this.setState(() => {
return {
data: this.store.get()
};
});
}
completeEditing(row, column, newValue) {
const { cellEdit, keyField } = this.props;
const { beforeSaveCell, onEditing, afterSaveCell } = cellEdit;
const oldValue = _.get(row, column.dataField);
const rowId = _.get(row, keyField);
if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column);
onEditing(rowId, column.dataField, newValue);
if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column);
this.setState(() => {
return {
data: this.store.get(),
currEditCell: { ridx: null, cidx: null }
};
});
}
startEditing(ridx, cidx) {
this.setState(() => {
return {
currEditCell: { ridx, cidx }
};
});
}
escapeEditing() {
this.setState(() => {
return {
currEditCell: { ridx: null, cidx: null }
data: store.get()
};
});
}
@@ -199,15 +167,27 @@ BootstrapTable.propTypes = {
]),
cellEdit: PropTypes.shape({
mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired,
onEditing: PropTypes.func.isRequired,
onUpdate: PropTypes.func,
onErrorMessageDisappear: PropTypes.func,
blurToSave: PropTypes.bool,
beforeSaveCell: PropTypes.func,
afterSaveCell: PropTypes.func,
nonEditableRows: PropTypes.func,
timeToCloseMessage: PropTypes.number
editing: PropTypes.bool,
timeToCloseMessage: PropTypes.number,
errorMessage: PropTypes.string
}),
selectRow: PropTypes.shape({
mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired
}),
onCellUpdate: PropTypes.func,
onStartEditing: PropTypes.func,
onEscapeEditing: PropTypes.func,
currEditCell: PropTypes.shape({
ridx: PropTypes.number,
cidx: PropTypes.number,
message: PropTypes.string,
editing: PropTypes.bool
})
};
+105
View File
@@ -0,0 +1,105 @@
/* eslint arrow-body-style: 0 */
/* eslint react/prop-types: 0 */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _ from './utils';
class CellEditWrapper extends Component {
constructor(props) {
super(props);
this.startEditing = this.startEditing.bind(this);
this.escapeEditing = this.escapeEditing.bind(this);
this.completeEditing = this.completeEditing.bind(this);
this.handleCellUpdate = this.handleCellUpdate.bind(this);
this.updateEditingWithErr = this.updateEditingWithErr.bind(this);
this.state = {
ridx: null,
cidx: null,
message: null,
editing: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.cellEdit) {
if (nextProps.cellEdit.editing) {
this.setState(() => {
return {
...this.state,
message: nextProps.cellEdit.errorMessage
};
});
} else {
this.escapeEditing();
}
}
}
handleCellUpdate(row, column, newValue) {
const { keyField, cellEdit, onUpdateCell } = this.props;
const { beforeSaveCell, afterSaveCell } = cellEdit;
const oldValue = _.get(row, column.dataField);
const rowId = _.get(row, keyField);
if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column);
if (onUpdateCell(rowId, column.dataField, newValue)) {
if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column);
this.completeEditing();
}
}
completeEditing() {
this.setState(() => {
return {
ridx: null,
cidx: null,
message: null,
editing: false
};
});
}
startEditing(ridx, cidx) {
this.setState(() => {
return {
ridx,
cidx,
editing: true
};
});
}
escapeEditing() {
this.setState(() => {
return {
ridx: null,
cidx: null,
editing: false
};
});
}
updateEditingWithErr(message) {
this.setState(() => {
return {
...this.state,
message
};
});
}
render() {
return React.cloneElement(this.props.elem, {
onCellUpdate: this.handleCellUpdate,
onStartEditing: this.startEditing,
onEscapeEditing: this.escapeEditing,
currEditCell: { ...this.state }
});
}
}
CellEditWrapper.propTypes = {
elem: PropTypes.element.isRequired,
onUpdateCell: PropTypes.func.isRequired
};
export default CellEditWrapper;
+27 -11
View File
@@ -23,6 +23,15 @@ class EditingCell extends Component {
};
}
componentWillReceiveProps({ message }) {
if (_.isDefined(message)) {
this.createTimer();
this.setState(() => {
return { invalidMessage: message };
});
}
}
componentWillUnmount() {
this.clearTimer();
}
@@ -33,24 +42,30 @@ class EditingCell extends Component {
}
}
beforeComplete(row, column, newValue) {
createTimer() {
this.clearTimer();
const { onComplete, timeToCloseMessage } = this.props;
const { timeToCloseMessage, onErrorMessageDisappear } = this.props;
this.indicatorTimer = _.sleep(() => {
this.setState(() => {
return { invalidMessage: null };
});
if (_.isFunction(onErrorMessageDisappear)) onErrorMessageDisappear();
}, timeToCloseMessage);
}
beforeComplete(row, column, newValue) {
const { onUpdate } = this.props;
if (_.isFunction(column.validator)) {
const validateForm = column.validator(newValue, row, column);
if (_.isObject(validateForm) && !validateForm.valid) {
this.setState(() => {
return { invalidMessage: validateForm.message };
});
this.indicatorTimer = setTimeout(() => {
this.setState(() => {
return { invalidMessage: null };
});
}, timeToCloseMessage);
this.createTimer();
return;
}
}
onComplete(row, column, newValue);
onUpdate(row, column, newValue);
}
handleBlur() {
@@ -90,7 +105,8 @@ class EditingCell extends Component {
onBlur: this.handleBlur
};
const editorClass = invalidMessage ? cs('animated', 'shake') : null;
const hasError = _.isDefined(invalidMessage);
const editorClass = hasError ? cs('animated', 'shake') : null;
return (
<td className="react-bootstrap-table-editing-cell">
<TextEditor
@@ -99,7 +115,7 @@ class EditingCell extends Component {
classNames={ editorClass }
{ ...editorAttrs }
/>
{ invalidMessage ? <EditorIndicator invalidMessage={ invalidMessage } /> : null }
{ hasError ? <EditorIndicator invalidMessage={ invalidMessage } /> : null }
</td>
);
}
@@ -108,7 +124,7 @@ class EditingCell extends Component {
EditingCell.propTypes = {
row: PropTypes.object.isRequired,
column: PropTypes.object.isRequired,
onComplete: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onEscape: PropTypes.func.isRequired,
timeToCloseMessage: PropTypes.number
};
@@ -18,13 +18,12 @@ export default ExtendBase =>
return this.props.data.length === 0;
}
resolveCellEditProps(options) {
resolveCellEditProps(options = { currEditCell: null }) {
const { cellEdit } = this.props;
const { currEditCell } = this.state;
const nonEditableRows =
(cellEdit && _.isFunction(cellEdit.nonEditableRows)) ? cellEdit.nonEditableRows() : [];
const cellEditInfo = {
...currEditCell,
...options.currEditCell,
nonEditableRows
};
@@ -72,9 +71,9 @@ export default ExtendBase =>
* @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;
resolveHeaderCellSelectionProps(options = {}) {
const { selectRow } = this.props;
const { allRowsSelected, selected = [], ...rest } = options;
const {
ROW_SELECT_DISABLED, CHECKBOX_STATUS_CHECKED,
CHECKBOX_STATUS_INDETERMINATE, CHECKBOX_STATUS_UNCHECKED
@@ -83,8 +82,6 @@ export default ExtendBase =>
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;
@@ -92,7 +89,7 @@ export default ExtendBase =>
return {
...selectRow,
...options,
...rest,
checkedStatus
};
}
+54 -8
View File
@@ -1,26 +1,72 @@
/* eslint arrow-body-style: 0 */
/* eslint react/jsx-no-bind: 0 */
/* eslint no-return-assign: 0 */
/* eslint react/prop-types: 0 */
import React, { Component } from 'react';
import Store from './store/base';
import CellEditWrapper from './cell-edit-wrapper';
import _ from './utils';
const withStateful = (Base) => {
class StatefulComponent extends Component {
constructor(props) {
super(props);
this.store = new Store(props);
this.edit = this.edit.bind(this);
this.handleUpdateCell = this.handleUpdateCell.bind(this);
}
edit(rowId, dataField, newValue) {
this.store.edit(rowId, dataField, newValue);
componentWillReceiveProps(nextProps) {
this.store.set(nextProps.data);
}
handleUpdateCell(rowId, dataField, newValue) {
const { cellEdit } = this.props;
// handle cell editing internal
if (!cellEdit.onUpdate) {
this.store.edit(rowId, dataField, newValue);
return true;
}
// handle cell editing external
const aPromise = cellEdit.onUpdate(rowId, dataField, newValue);
if (_.isDefined(aPromise) && aPromise !== false) { // TODO: should be a promise here
aPromise.then((result = true) => {
const response = result === true ? {} : result;
if (_.isObject(response)) {
const { value } = response;
this.store.edit(rowId, dataField, value || newValue);
this.table.completeEditing();
}
}).catch((e) => {
this.table.updateEditingWithErr(e.message);
});
}
return false;
}
renderCellEdit(elem) {
return (
<CellEditWrapper
keyField={ this.props.keyField }
cellEdit={ this.props.cellEdit }
ref={ node => this.table = node }
elem={ elem }
onUpdateCell={ this.handleUpdateCell }
/>
);
}
render() {
const { props } = this;
const newProps = { ...props };
if (newProps.cellEdit && !newProps.cellEdit.onEditing) {
newProps.cellEdit.onEditing = this.edit;
const baseProps = {
...this.props,
store: this.store
};
let element = React.createElement(Base, baseProps);
if (this.props.cellEdit) {
element = this.renderCellEdit(element);
}
return <Base { ...newProps } store={ this.store } />;
return element;
}
}
return StatefulComponent;
+6 -1
View File
@@ -1,3 +1,4 @@
/* eslint class-methods-use-this: 0 */
import { sort } from './sort';
import Const from '../const';
import _ from '../utils';
@@ -6,7 +7,7 @@ export default class Store {
constructor(props) {
const { data, keyField } = props;
this.keyField = keyField;
this.data = data ? data.slice() : [];
this.set(data);
this.sortOrder = undefined;
this.sortField = undefined;
@@ -37,6 +38,10 @@ export default class Store {
return this.data;
}
set(data) {
this.data = data ? JSON.parse(JSON.stringify(data)) : [];
}
getRowByRowId(rowId) {
return this.get().find(row => _.get(row, this.keyField) === rowId);
}
+6 -1
View File
@@ -68,11 +68,16 @@ function isDefined(value) {
return typeof value !== 'undefined' && value !== null;
}
function sleep(fn, ms) {
return setTimeout(() => fn(), ms);
}
export default {
get,
set,
isFunction,
isObject,
isEmptyObject,
isDefined
isDefined,
sleep
};
+38 -14
View File
@@ -3,6 +3,7 @@ import sinon from 'sinon';
import { shallow } from 'enzyme';
import Caption from '../src/caption';
import Store from '../src/store/base';
import Header from '../src/header';
import Body from '../src/body';
import BootstrapTable from '../src/bootstrap-table';
@@ -26,9 +27,12 @@ describe('BootstrapTable', () => {
name: 'B'
}];
const store = new Store({ data });
describe('simplest table', () => {
beforeEach(() => {
wrapper = shallow(<BootstrapTable keyField="id" columns={ columns } data={ data } />);
wrapper = shallow(
<BootstrapTable keyField="id" columns={ columns } data={ data } store={ store } />);
});
it('should render successfully', () => {
@@ -40,9 +44,8 @@ describe('BootstrapTable', () => {
});
it('should have correct default state', () => {
expect(wrapper.state().currEditCell).toBeDefined();
expect(wrapper.state().currEditCell.ridx).toBeNull();
expect(wrapper.state().currEditCell.cidx).toBeNull();
expect(wrapper.state().data).toBeDefined();
expect(wrapper.state().data).toEqual(store.get());
});
it('should have table-bordered class as default', () => {
@@ -52,7 +55,8 @@ describe('BootstrapTable', () => {
describe('when hover props is true', () => {
beforeEach(() => {
wrapper = shallow(<BootstrapTable keyField="id" columns={ columns } data={ data } hover />);
wrapper = shallow(
<BootstrapTable keyField="id" columns={ columns } data={ data } store={ store } hover />);
});
it('should have table-hover class on table', () => {
@@ -62,7 +66,8 @@ describe('BootstrapTable', () => {
describe('when striped props is true', () => {
beforeEach(() => {
wrapper = shallow(<BootstrapTable keyField="id" columns={ columns } data={ data } striped />);
wrapper = shallow(
<BootstrapTable keyField="id" columns={ columns } data={ data } store={ store } striped />);
});
it('should have table-striped class on table', () => {
@@ -72,7 +77,8 @@ describe('BootstrapTable', () => {
describe('when condensed props is true', () => {
beforeEach(() => {
wrapper = shallow(<BootstrapTable keyField="id" columns={ columns } data={ data } condensed />);
wrapper = shallow(
<BootstrapTable keyField="id" columns={ columns } data={ data } store={ store } condensed />);
});
it('should have table-condensed class on table', () => {
@@ -82,7 +88,8 @@ describe('BootstrapTable', () => {
describe('when bordered props is false', () => {
beforeEach(() => {
wrapper = shallow(<BootstrapTable keyField="id" columns={ columns } data={ data } bordered={ false } />);
wrapper = shallow(
<BootstrapTable keyField="id" columns={ columns } data={ data } store={ store } bordered={ false } />);
});
it('should not have table-condensed class on table', () => {
@@ -94,6 +101,7 @@ describe('BootstrapTable', () => {
beforeEach(() => {
wrapper = shallow(
<BootstrapTable
store={ store }
caption={ <span className="table-caption">test</span> }
keyField="id"
columns={ columns }
@@ -111,6 +119,12 @@ describe('BootstrapTable', () => {
describe('when cellEdit props is defined', () => {
const nonEditableRows = [data[1].id];
const currEditCell = {
ridx: 1,
cidx: 2,
message: null,
editing: false
};
const cellEdit = {
mode: Const.CLICK_TO_CELL_EDIT,
onEditing: sinon.stub(),
@@ -124,7 +138,12 @@ describe('BootstrapTable', () => {
columns={ columns }
data={ data }
bordered={ false }
store={ store }
cellEdit={ cellEdit }
onCellUpdate={ sinon.stub() }
onStartEditing={ sinon.stub() }
onEscapeEditing={ sinon.stub() }
currEditCell={ currEditCell }
/>
);
});
@@ -133,11 +152,13 @@ describe('BootstrapTable', () => {
const body = wrapper.find(Body);
expect(body.length).toBe(1);
expect(body.props().cellEdit.nonEditableRows).toEqual(nonEditableRows);
expect(body.props().cellEdit.ridx).toEqual(wrapper.state().currEditCell.ridx);
expect(body.props().cellEdit.cidx).toEqual(wrapper.state().currEditCell.cidx);
expect(body.props().cellEdit.ridx).toEqual(currEditCell.ridx);
expect(body.props().cellEdit.cidx).toEqual(currEditCell.cidx);
expect(body.props().cellEdit.message).toEqual(currEditCell.message);
expect(body.props().cellEdit.editing).toEqual(currEditCell.editing);
expect(body.props().cellEdit.onStart).toBeDefined();
expect(body.props().cellEdit.onEscape).toBeDefined();
expect(body.props().cellEdit.onComplete).toBeDefined();
expect(body.props().cellEdit.onUpdate).toBeDefined();
});
});
@@ -149,6 +170,7 @@ describe('BootstrapTable', () => {
wrapper = shallow(
<BootstrapTable
keyField="id"
store={ store }
columns={ columns }
data={ data }
selectRow={{ mode: 'radio' }}
@@ -170,6 +192,7 @@ describe('BootstrapTable', () => {
wrapper = shallow(
<BootstrapTable
keyField="id"
store={ store }
columns={ columns }
data={ data }
selectRow={{ mode: 'checkbox' }}
@@ -199,6 +222,7 @@ describe('BootstrapTable', () => {
wrapper = shallow(
<BootstrapTable
keyField="id"
store={ store }
columns={ columns }
data={ data }
/>
@@ -208,7 +232,7 @@ describe('BootstrapTable', () => {
describe('when customized option was not given', () => {
describe('when nothing was selected', () => {
it('should select all rows', () => {
wrapper.instance().store.setSelectedRowKeys([]);
store.setSelectedRowKeys([]);
wrapper.instance().handleAllRowsSelect();
@@ -218,7 +242,7 @@ describe('BootstrapTable', () => {
describe('when one or more than one row was selected', () => {
it('should unselect all rows', () => {
wrapper.instance().store.setSelectedRowKeys([1]);
store.setSelectedRowKeys([1]);
wrapper.instance().handleAllRowsSelect();
@@ -238,7 +262,7 @@ describe('BootstrapTable', () => {
describe('when option is falsy', () => {
it('should unselect all rows', () => {
wrapper.instance().store.setSelectedRowKeys([1]);
store.setSelectedRowKeys([1]);
wrapper.instance().handleAllRowsSelect(false);
@@ -0,0 +1,268 @@
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import Store from '../src/store/base';
import BootstrapTable from '../src/bootstrap-table';
import CellEditWrapper from '../src/cell-edit-wrapper';
describe('CellEditWrapper', () => {
let wrapper;
let elem;
const columns = [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'name',
text: 'Name'
}];
const data = [{
id: 1,
name: 'A'
}, {
id: 2,
name: 'B'
}];
const cellEdit = {
mode: 'click'
};
const keyField = 'id';
const store = new Store({ data, keyField });
beforeEach(() => {
elem = React.createElement(BootstrapTable, { data, cellEdit, columns, keyField, store });
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ sinon.stub() }
/>
);
});
it('should render CellEditWrapper correctly', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find(BootstrapTable)).toBeDefined();
});
it('should have correct state', () => {
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().message).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
});
it('should inject correct props to elem', () => {
expect(wrapper.props().onCellUpdate).toBeDefined();
expect(wrapper.props().onStartEditing).toBeDefined();
expect(wrapper.props().onEscapeEditing).toBeDefined();
expect(wrapper.props().currEditCell).toBeDefined();
expect(wrapper.props().currEditCell.ridx).toBeNull();
expect(wrapper.props().currEditCell.cidx).toBeNull();
expect(wrapper.props().currEditCell.message).toBeNull();
expect(wrapper.props().currEditCell.editing).toBeFalsy();
});
describe('when receive new cellEdit prop', () => {
const spy = jest.spyOn(CellEditWrapper.prototype, 'escapeEditing');
describe('and cellEdit.editing is false', () => {
beforeEach(() => {
elem = React.createElement(BootstrapTable, { data, cellEdit, columns, keyField, store });
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ sinon.stub() }
/>
);
wrapper.setProps({ cellEdit: { ...cellEdit, editing: false } });
});
it('should call escapeEditing', () => {
expect(spy).toHaveBeenCalled();
});
it('should have correct state', () => {
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().message).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
});
});
describe('and cellEdit.editing is true', () => {
const errorMessage = 'test';
const ridx = 1;
const cidx = 2;
beforeEach(() => {
elem = React.createElement(BootstrapTable, { data, cellEdit, columns, keyField, store });
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ sinon.stub() }
/>
);
wrapper.setState({ ridx, cidx, editing: true });
wrapper.setProps({ cellEdit: { ...cellEdit, editing: true, errorMessage } });
});
it('should have correct state', () => {
expect(wrapper.state().ridx).toEqual(ridx);
expect(wrapper.state().cidx).toEqual(cidx);
expect(wrapper.state().editing).toBeTruthy();
expect(wrapper.state().message).toEqual(errorMessage);
});
});
});
describe('call updateEditingWithErr function', () => {
it('should set state.message correctly', () => {
const message = 'test';
wrapper.instance().updateEditingWithErr(message);
expect(wrapper.state().message).toEqual(message);
});
});
describe('call escapeEditing function', () => {
it('should set state correctly', () => {
wrapper.instance().escapeEditing();
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
});
});
describe('call startEditing function', () => {
it('should set state correctly', () => {
const ridx = 1;
const cidx = 3;
wrapper.instance().startEditing(ridx, cidx);
expect(wrapper.state().ridx).toEqual(ridx);
expect(wrapper.state().cidx).toEqual(cidx);
expect(wrapper.state().editing).toBeTruthy();
});
});
describe('call completeEditing function', () => {
it('should set state correctly', () => {
wrapper.instance().completeEditing();
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().message).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
});
});
describe('call handleCellUpdate function', () => {
let onUpdateCellCallBack;
const row = data[0];
const column = columns[1];
const newValue = 'new name';
beforeEach(() => {
onUpdateCellCallBack = sinon.stub().returns(true);
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ onUpdateCellCallBack }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
});
afterEach(() => { onUpdateCellCallBack.reset(); });
it('should calling onUpdateCell callback correctly', () => {
expect(onUpdateCellCallBack.callCount).toBe(1);
expect(onUpdateCellCallBack.calledWith(row.id, column.dataField, newValue)).toBe(true);
});
describe('when onUpdateCell function return true', () => {
const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing');
it('should calling completeEditing function', () => {
expect(spy).toHaveBeenCalled();
});
describe('if cellEdit.afterSaveCell prop defined', () => {
const aftereSaveCellCallBack = sinon.stub();
beforeEach(() => {
cellEdit.beforeSaveCell = aftereSaveCellCallBack;
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ onUpdateCellCallBack }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
});
it('should calling cellEdit.afterSaveCell correctly', () => {
expect(aftereSaveCellCallBack.callCount).toBe(1);
expect(aftereSaveCellCallBack.calledWith(
row[column.dataField], newValue, row, column)
).toBe(true);
});
});
});
describe('when onUpdateCell function return false', () => {
const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing');
beforeEach(() => {
onUpdateCellCallBack = sinon.stub().returns(false);
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ onUpdateCellCallBack }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
});
it('shouldn\'t calling completeEditing function', () => {
expect(spy).toHaveBeenCalled();
});
});
describe('if cellEdit.beforeSaveCell prop defined', () => {
const beforeSaveCellCallBack = sinon.stub();
beforeEach(() => {
cellEdit.beforeSaveCell = beforeSaveCellCallBack;
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
cellEdit={ cellEdit }
elem={ elem }
onUpdateCell={ onUpdateCellCallBack }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
});
it('should calling cellEdit.beforeSaveCell correctly', () => {
expect(beforeSaveCellCallBack.callCount).toBe(1);
expect(beforeSaveCellCallBack.calledWith(
row[column.dataField], newValue, row, column)
).toBe(true);
});
});
});
});
@@ -10,8 +10,8 @@ import EditorIndicator from '../src/editor-indicator';
describe('EditingCell', () => {
let wrapper;
let onUpdate;
let onEscape;
let onComplete;
const row = {
id: 1,
name: 'A'
@@ -23,14 +23,14 @@ describe('EditingCell', () => {
};
beforeEach(() => {
onComplete = sinon.stub();
onEscape = sinon.stub();
onUpdate = sinon.stub();
wrapper = shallow(
<EditingCell
row={ row }
column={ column }
onUpdate={ onUpdate }
onEscape={ onEscape }
onComplete={ onComplete }
/>
);
});
@@ -55,12 +55,12 @@ describe('EditingCell', () => {
expect(indicator.length).toEqual(0);
});
it('when press ENTER on TextEditor should call onComplete correctly', () => {
it('when press ENTER on TextEditor should call onUpdate correctly', () => {
const newValue = 'test';
const textEditor = wrapper.find(TextEditor);
textEditor.simulate('keyDown', { keyCode: 13, currentTarget: { value: newValue } });
expect(onComplete.callCount).toBe(1);
expect(onComplete.calledWith(row, column, newValue)).toBe(true);
expect(onUpdate.callCount).toBe(1);
expect(onUpdate.calledWith(row, column, newValue)).toBe(true);
});
it('when press ESC on TextEditor should call onEscape correctly', () => {
@@ -82,19 +82,19 @@ describe('EditingCell', () => {
<EditingCell
row={ row }
column={ column }
onUpdate={ onUpdate }
onEscape={ onEscape }
onComplete={ onComplete }
blurToSave
/>
</TableRowWrapper>
);
});
it('when blur from TextEditor should call onComplete correctly', () => {
it('when blur from TextEditor should call onUpdate correctly', () => {
const textEditor = wrapper.find(TextEditor);
textEditor.simulate('blur');
expect(onComplete.callCount).toBe(1);
expect(onComplete.calledWith(row, column, `${row[column.dataField]}`)).toBe(true);
expect(onUpdate.callCount).toBe(1);
expect(onUpdate.calledWith(row, column, `${row[column.dataField]}`)).toBe(true);
});
});
@@ -121,8 +121,8 @@ describe('EditingCell', () => {
expect(validatorCallBack.calledWith(newValue, row, column)).toBe(true);
});
it('should not call onComplete', () => {
expect(onComplete.callCount).toBe(0);
it('should not call onUpdate', () => {
expect(onUpdate.callCount).toBe(0);
});
it('should set indicatorTimer successfully', () => {
@@ -164,8 +164,8 @@ describe('EditingCell', () => {
expect(validatorCallBack.calledWith(newValue, row, column)).toBe(true);
});
it('should call onComplete', () => {
expect(onComplete.callCount).toBe(1);
it('should call onUpdate', () => {
expect(onUpdate.callCount).toBe(1);
});
});
});
@@ -86,18 +86,6 @@ describe('TableResolver', () => {
expect(cellEdit).toBeDefined();
expect(cellEdit.mode).toEqual(Const.UNABLE_TO_CELL_EDIT);
expect(cellEdit.nonEditableRows.length).toEqual(0);
expect(cellEdit.ridx).toBeNull();
expect(cellEdit.cidx).toBeNull();
});
it('should resolve a default cellEdit instance even if state.currEditCell changed', () => {
const ridx = 1;
const cidx = 1;
wrapper.setState({ currEditCell: { ridx, cidx } });
const cellEdit = wrapper.instance().resolveCellEditProps();
expect(cellEdit).toBeDefined();
expect(cellEdit.ridx).toEqual(ridx);
expect(cellEdit.cidx).toEqual(cidx);
});
});
@@ -105,7 +93,7 @@ describe('TableResolver', () => {
const expectNonEditableRows = [1, 2];
const cellEdit = {
mode: Const.DBCLICK_TO_CELL_EDIT,
onEditing: sinon.stub(),
onUpdate: sinon.stub(),
blurToSave: true,
beforeSaveCell: sinon.stub(),
afterSaveCell: sinon.stub(),
@@ -122,10 +110,8 @@ describe('TableResolver', () => {
it('should resolve a cellEdit correctly', () => {
const cellEditInfo = wrapper.instance().resolveCellEditProps();
expect(cellEditInfo).toBeDefined();
expect(cellEditInfo.ridx).toBeNull();
expect(cellEditInfo.cidx).toBeNull();
expect(cellEditInfo.mode).toEqual(cellEdit.mode);
expect(cellEditInfo.onEditing).toEqual(cellEdit.onEditing);
expect(cellEditInfo.onUpdate).toEqual(cellEdit.onUpdate);
expect(cellEditInfo.blurToSave).toEqual(cellEdit.blurToSave);
expect(cellEditInfo.beforeSaveCell).toEqual(cellEdit.beforeSaveCell);
expect(cellEditInfo.afterSaveCell).toEqual(cellEdit.afterSaveCell);
@@ -280,7 +266,9 @@ describe('TableResolver', () => {
selectRow = {};
const mockOptions = {
foo: 'test',
bar: sinon.stub()
bar: sinon.stub(),
allRowsSelected: false,
selected: []
};
const selectedRowKeys = [];
const mockElement = React.createElement(BootstrapTableMock, {
@@ -290,12 +278,20 @@ describe('TableResolver', () => {
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps(mockOptions);
});
it('should return object which contain options', () => {
it('should return object which contain specified options', () => {
expect(headerCellSelectionInfo).toEqual(expect.objectContaining({
foo: 'test',
bar: expect.any(Function)
}));
});
it('should return object which can not contain allRowsSelected option', () => {
expect(headerCellSelectionInfo.allRowsSelected).not.toBeDefined();
});
it('should return object which can not contain allRowsSelected option', () => {
expect(headerCellSelectionInfo.selected).not.toBeDefined();
});
});
describe('if all rows were selected', () => {
@@ -307,9 +303,11 @@ describe('TableResolver', () => {
}, null);
wrapper = shallow(mockElement);
wrapper.instance().store.setSelectedRowKeys(selectedRowKeys);
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps();
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps({
allRowsSelected: true,
selected: selectedRowKeys
});
});
it('should return checkedStatus which eqauls to checked', () => {
@@ -318,6 +316,7 @@ describe('TableResolver', () => {
}));
});
});
describe('if part of rows were selected', () => {
beforeEach(() => {
selectRow = {};
@@ -327,8 +326,10 @@ describe('TableResolver', () => {
}, null);
wrapper = shallow(mockElement);
wrapper.instance().store.setSelectedRowKeys(selectedRowKeys);
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps();
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps({
allRowsSelected: false,
selected: selectedRowKeys
});
});
it('should return checkedStatus which eqauls to indeterminate', () => {
@@ -347,9 +348,11 @@ describe('TableResolver', () => {
}, null);
wrapper = shallow(mockElement);
wrapper.instance().store.setSelectedRowKeys(selectedRowKeys);
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps();
headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps({
allRowsSelected: false,
selected: selectedRowKeys
});
});
it('should return checkedStatus which eqauls to unchecked', () => {
@@ -225,8 +225,9 @@ describe('Row', () => {
beforeEach(() => {
cellEdit.ridx = rowIndex;
cellEdit.cidx = editingColIndex;
cellEdit.onComplete = sinon.stub();
cellEdit.onUpdate = sinon.stub();
cellEdit.onEscape = sinon.stub();
cellEdit.onUpdate = sinon.stub();
wrapper = shallow(
<Row
{...mockBodyResolvedProps}
@@ -255,7 +256,7 @@ describe('Row', () => {
beforeEach(() => {
cellEdit.ridx = 3;
cellEdit.cidx = editingColIndex;
cellEdit.onComplete = sinon.stub();
cellEdit.onUpdate = sinon.stub();
cellEdit.onEscape = sinon.stub();
wrapper = shallow(
<Row
@@ -0,0 +1,133 @@
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { BootstrapTable, BootstrapTableful } from '../src';
describe('withStateful', () => {
let wrapper;
const keyField = 'id';
const columns = [{
dataField: keyField,
text: 'ID'
}, {
dataField: 'name',
text: 'Name'
}];
const data = [{
id: 1,
name: 'A'
}, {
id: 2,
name: 'B'
}];
describe('initialization', () => {
beforeEach(() => {
wrapper = shallow(
<BootstrapTableful keyField={ keyField } data={ data } columns={ columns } />
);
});
it('should render BootstrapTable successfully', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find(BootstrapTable).length).toBe(1);
});
it('should creating store successfully', () => {
const store = wrapper.instance().store;
expect(store).toBeDefined();
expect(store.get()).toEqual(data);
expect(store.keyField).toEqual(keyField);
});
});
describe('when cellEdit is defined', () => {
const spy = jest.spyOn(BootstrapTableful.prototype, 'renderCellEdit');
const cellEdit = {
mode: 'click'
};
beforeEach(() => {
wrapper = shallow(
<BootstrapTableful
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
/>
);
});
it('should calling renderCellEdit function', () => {
expect(spy).toHaveBeenCalled();
});
it('should injecting correct props', () => {
expect(wrapper.props().keyField).toEqual('id');
expect(wrapper.props().cellEdit).toEqual(cellEdit);
expect(wrapper.props().elem).toBeDefined();
expect(wrapper.props().onUpdateCell).toBeDefined();
});
describe('for handleUpdateCell function', () => {
const rowId = data[1].id;
const dataField = columns[1].dataField;
const newValue = 'tester';
let result;
describe('when cellEdit.onUpdate callback is not defined', () => {
beforeEach(() => {
result = wrapper.instance().handleUpdateCell(rowId, dataField, newValue);
});
it('should return true', () => {
expect(result).toBeTruthy();
});
it('should update store data directly', () => {
const store = wrapper.instance().store;
const row = store.getRowByRowId(rowId);
expect(row[dataField]).toEqual(newValue);
});
});
describe('when cellEdit.onUpdate callback is define and which return false', () => {
beforeEach(() => {
cellEdit.onUpdate = sinon.stub().returns(false);
wrapper = shallow(
<BootstrapTableful
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
/>
);
result = wrapper.instance().handleUpdateCell(rowId, dataField, newValue);
});
it('should calling cellEdit.onUpdate callback correctly', () => {
expect(cellEdit.onUpdate.callCount).toBe(1);
expect(cellEdit.onUpdate.calledWith(rowId, dataField, newValue)).toBe(true);
});
it('should return false', () => {
expect(result).toBeFalsy();
});
it('shouldn\'t update store data', () => {
const store = wrapper.instance().store;
const row = store.getRowByRowId(rowId);
expect(row[dataField]).not.toEqual(newValue);
});
});
// We need refactoring handleUpdateCell function for handling promise firstly
// then it will be much easier to test
describe.skip('when cellEdit.onUpdate callback is define and which return a Promise', () => {});
});
});
});
@@ -8,13 +8,7 @@ export const extendTo = Base =>
const { data } = props;
this.store = new Store(props);
this.state = {
data,
currEditCell: {
ridx: null,
cidx: null
}
};
this.state = { data };
}
render() { return null; }