Refine cell edit remote mode
This commit is contained in:
Allen 2018-01-06 13:48:07 +08:00 committed by GitHub
commit 6913434714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 425 additions and 581 deletions

View File

@ -40,7 +40,13 @@ This is a chance that you can connect to your remote server or database to manip
For flexibility reason, you can control what functionality should be handled on remote via a object return:
```js
remote={ { filter: true } }
remote={ {
filter: true,
pagination: true,
filter: true,
sort: true,
cellEdit: true
} }
```
In above case, only column filter will be handled on remote.
@ -53,7 +59,7 @@ A special case for remote pagination:
remote={ { pagination: true, filter: false, sort: false } }
```
In pagination case, even you only specified the paignation need to handle as remote, `react-bootstrap-table2` will handle all the table changes(`filter`, `sort` etc) as remote mode, because `react-bootstrap-table` only know the data of current page, but filtering, searching or sort need to work on overall datas.
In pagination case, even you only specified the paignation need to handle as remote, `react-bootstrap-table2` will handle all the table changes(`filter`, `sort`) as remote mode, because `react-bootstrap-table` only know the data of current page, but filtering, searching or sort need to work on overall datas.
### <a name='loading'>loading - [Bool]</a>
Telling if table is loading or not, for example: waiting data loading, filtering etc. It's **only** valid when [`remote`](#remote) is enabled.
@ -250,6 +256,7 @@ There's only two arguments will be passed to `onTableChange`: `type` and `newSta
* `filter`
* `pagination`
* `sort`
* `cellEdit`
Following is a shape of `newState`
@ -260,6 +267,11 @@ Following is a shape of `newState`
sortField, // newest sort field
sortOrder, // newest sort order
filters, // an object which have current filter status per column
data // when you enable remote sort, you may need to base on data to sort if data is filtered/searched
data, // when you enable remote sort, you may need to base on data to sort if data is filtered/searched
cellEdit: { // You can only see this prop when type is cellEdit
rowId,
dataField,
newValue
}
}
```

View File

@ -5,8 +5,6 @@
* [timeToCloseMessage](#timeToCloseMessage)
* [beforeSaveCell](#beforeSaveCell)
* [afterSaveCell](#afterSaveCell)
* [onUpdate](#onUpdate)
* [editing](#editing)
* [errorMessage](#errorMessage)
* [onErrorMessageDisappear](#onErrorMessageDisappear)
@ -21,9 +19,7 @@ Following is the shape of `cellEdit` object:
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: () => { ... },
@ -63,73 +59,8 @@ const cellEdit = {
};
```
### <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.
This prop is not often used. Only used when you keep the error message in your application state and also handle the cell editing on remote mode.
### <a name='onErrorMessageDisappear'>cellEdit.onErrorMessageDisappear - [Function]</a>
This callback function will be called when error message discard.

View File

@ -1,75 +0,0 @@
/* eslint no-unused-vars: 0 */
/* eslint arrow-body-style: 0 */
import React, { Component } from 'react';
import BootstrapTable 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>
<BootstrapTable 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>
<BootstrapTable keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);
}
}
export default CellEditWithPromise;

View File

@ -1,212 +0,0 @@
/* 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 BootstrapTable 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>
<BootstrapTable 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>
<BootstrapTable 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;

View File

@ -0,0 +1,165 @@
import React from 'react';
import PropTypes from 'prop-types';
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 sourceCode = `\
const RemoteCellEdit = (props) => {
const cellEdit = {
mode: 'click',
errorMessage: props.errorMessage
};
return (
<div>
<BootstrapTable
remote={ { cellEdit: true } }
keyField="id"
data={ props.data }
columns={ columns }
cellEdit={ cellEdit }
onTableChange={ props.onTableChange }
/>
<Code>{ sourceCode }</Code>
</div>
);
};
RemoteCellEdit.propTypes = {
data: PropTypes.array.isRequired,
onTableChange: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired
};
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
data: products,
errorMessage: null
};
}
handleTableChange = (type, { data, cellEdit: { rowId, dataField, newValue } }) => {
setTimeout(() => {
if (newValue === 'test' && dataField === 'name') {
this.setState(() => ({
data,
errorMessage: 'Oops, product name shouldn't be "test"'
}));
} else {
const result = data.map((row) => {
if (row.id === rowId) {
const newRow = { ...row };
newRow[dataField] = newValue;
return newRow;
}
return row;
});
this.setState(() => ({
data: result,
errorMessage: null
}));
}
}, 2000);
}
render() {
return (
<RemoteCellEdit
data={ this.state.data }
errorMessage={ this.state.errorMessage }
onTableChange={ this.handleTableChange }
/>
);
}
}
`;
const RemoteCellEdit = (props) => {
const cellEdit = {
mode: 'click',
errorMessage: props.errorMessage
};
return (
<div>
<BootstrapTable
remote={ { cellEdit: true } }
keyField="id"
data={ props.data }
columns={ columns }
cellEdit={ cellEdit }
onTableChange={ props.onTableChange }
/>
<Code>{ sourceCode }</Code>
</div>
);
};
RemoteCellEdit.propTypes = {
data: PropTypes.array.isRequired,
onTableChange: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired
};
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
data: products,
errorMessage: null
};
}
handleTableChange = (type, { data, cellEdit: { rowId, dataField, newValue } }) => {
setTimeout(() => {
if (newValue === 'test' && dataField === 'name') {
this.setState(() => ({
data,
errorMessage: 'Oops, product name shouldn\'t be "test"'
}));
} else {
const result = data.map((row) => {
if (row.id === rowId) {
const newRow = { ...row };
newRow[dataField] = newValue;
return newRow;
}
return row;
});
this.setState(() => ({
data: result,
errorMessage: null
}));
}
}, 2000);
}
render() {
return (
<RemoteCellEdit
data={ this.state.data }
errorMessage={ this.state.errorMessage }
onTableChange={ this.handleTableChange }
/>
);
}
}
export default Container;

View File

@ -63,8 +63,6 @@ import CellEditHooks from 'examples/cell-edit/cell-edit-hooks-table';
import CellEditValidator from 'examples/cell-edit/cell-edit-validator-table';
import CellEditStyleTable from 'examples/cell-edit/cell-edit-style-table';
import CellEditClassTable from 'examples/cell-edit/cell-edit-class-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';
@ -91,6 +89,7 @@ import TableOverlay from 'examples/loading-overlay/table-overlay';
import RemoteSort from 'examples/remote/remote-sort';
import RemoteFilter from 'examples/remote/remote-filter';
import RemotePaginationTable from 'examples/remote/remote-pagination';
import RemoteCellEdit from 'examples/remote/remote-celledit';
import RemoteAll from 'examples/remote/remote-all';
// css style
@ -165,9 +164,7 @@ storiesOf('Cell Editing', module)
.add('Rich Hook Functions', () => <CellEditHooks />)
.add('Validation', () => <CellEditValidator />)
.add('Custom Cell Style When Editing', () => <CellEditStyleTable />)
.add('Custom Cell Classes When Editing', () => <CellEditClassTable />)
.add('Async Cell Editing(Promise)', () => <CellEditWithPromise />)
.add('Async Cell Editing(Redux)', () => <CellEditWithRedux />);
.add('Custom Cell Classes When Editing', () => <CellEditClassTable />);
storiesOf('Row Selection', module)
.add('Single Selection', () => <SingleSelectionTable />)
@ -194,4 +191,5 @@ storiesOf('Remote', module)
.add('Remote Sort', () => <RemoteSort />)
.add('Remote Filter', () => <RemoteFilter />)
.add('Remote Pagination', () => <RemotePaginationTable />)
.add('Remote Cell Editing', () => <RemoteCellEdit />)
.add('Remote All', () => <RemoteAll />);

View File

@ -1,3 +1,5 @@
/* eslint no-param-reassign: 0 */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { filters } from './filter';
@ -15,14 +17,20 @@ export default (Base, {
constructor(props) {
super(props);
this.state = { currFilters: {}, isDataChanged: false };
this.state = { currFilters: {}, isDataChanged: props.isDataChanged || false };
this.onFilter = this.onFilter.bind(this);
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps({ isDataChanged, store, columns }) {
// consider to use lodash.isEqual
if (JSON.stringify(this.state.currFilters) !== JSON.stringify(nextProps.store.filters)) {
this.setState(() => ({ isDataChanged: true, currFilters: nextProps.store.filters }));
if (JSON.stringify(this.state.currFilters) !== JSON.stringify(store.filters)) {
this.setState(() => ({ isDataChanged: true, currFilters: store.filters }));
} else if (isDataChanged) {
if (!(this.isRemoteFiltering() || this.isRemotePagination()) &&
Object.keys(this.state.currFilters).length > 0) {
store.filteredData = filters(store, columns, _)(this.state.currFilters);
}
this.setState(() => ({ isDataChanged }));
} else {
this.setState(() => ({ isDataChanged: false }));
}

View File

@ -28,7 +28,7 @@ describe('Wrapper', () => {
onTableChangeCB.reset();
});
const createTableProps = () => {
const createTableProps = (props) => {
const tableProps = {
keyField: 'id',
columns: [{
@ -47,7 +47,8 @@ describe('Wrapper', () => {
filter: filter(),
_,
store: new Store('id'),
onTableChange: onTableChangeCB
onTableChange: onTableChangeCB,
...props
};
tableProps.store.data = data;
return tableProps;
@ -105,6 +106,17 @@ describe('Wrapper', () => {
});
});
describe('when props.isDataChanged is true and remote is enable', () => {
beforeEach(() => {
nextProps = createTableProps({ isDataChanged: true });
instance.componentWillReceiveProps(nextProps);
});
it('should setting isDataChanged as true', () => {
expect(instance.state.isDataChanged).toBeTruthy();
});
});
describe('when props.store.filters is different from current state.currFilters', () => {
beforeEach(() => {
nextProps = createTableProps();

View File

@ -130,13 +130,11 @@ BootstrapTable.propTypes = {
filter: PropTypes.object,
cellEdit: PropTypes.shape({
mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired,
onUpdate: PropTypes.func,
onErrorMessageDisappear: PropTypes.func,
blurToSave: PropTypes.bool,
beforeSaveCell: PropTypes.func,
afterSaveCell: PropTypes.func,
nonEditableRows: PropTypes.func,
editing: PropTypes.bool,
timeToCloseMessage: PropTypes.number,
errorMessage: PropTypes.string
}),
@ -146,8 +144,7 @@ BootstrapTable.propTypes = {
currEditCell: PropTypes.shape({
ridx: PropTypes.number,
cidx: PropTypes.number,
message: PropTypes.string,
editing: PropTypes.bool
message: PropTypes.string
}),
selectRow: PropTypes.shape({
mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired,

View File

@ -1,44 +1,54 @@
/* eslint react/prop-types: 0 */
import React, { Component } from 'react';
import _ from '../utils';
import remoteResolver from '../props-resolver/remote-resolver';
export default (Base, parentProps) =>
class CellEditWrapper extends Component {
export default Base =>
class CellEditWrapper extends remoteResolver(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
isDataChanged: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.cellEdit) {
if (nextProps.cellEdit.editing) {
if (nextProps.cellEdit && this.isRemoteCellEdit()) {
if (nextProps.cellEdit.errorMessage) {
this.setState(() => ({
...this.state,
isDataChanged: false,
message: nextProps.cellEdit.errorMessage
}));
} else {
this.setState(() => ({
isDataChanged: true
}));
this.escapeEditing();
}
} else {
this.setState(() => ({
isDataChanged: false
}));
}
}
handleCellUpdate(row, column, newValue) {
const { keyField, cellEdit } = this.props;
const { keyField, cellEdit, store } = 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 (parentProps.onUpdateCell(rowId, column.dataField, newValue)) {
if (this.isRemoteCellEdit()) {
this.handleCellChange(rowId, column.dataField, newValue);
} else {
store.edit(rowId, column.dataField, newValue);
if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column);
this.completeEditing();
}
@ -49,7 +59,7 @@ export default (Base, parentProps) =>
ridx: null,
cidx: null,
message: null,
editing: false
isDataChanged: true
}));
}
@ -58,7 +68,7 @@ export default (Base, parentProps) =>
this.setState(() => ({
ridx,
cidx,
editing: true
isDataChanged: false
}));
};
@ -69,27 +79,21 @@ export default (Base, parentProps) =>
escapeEditing() {
this.setState(() => ({
ridx: null,
cidx: null,
editing: false
}));
}
updateEditingWithErr(message) {
this.setState(() => ({
...this.state,
message
cidx: null
}));
}
render() {
const { isDataChanged, ...rest } = this.state;
return (
<Base
{ ...this.props }
isDataChanged={ isDataChanged }
data={ this.props.store.data }
onCellUpdate={ this.handleCellUpdate }
onStartEditing={ this.startEditing }
onEscapeEditing={ this.escapeEditing }
currEditCell={ { ...this.state } }
currEditCell={ { ...rest } }
/>
);
}

View File

@ -16,11 +16,10 @@ const withDataStore = Base =>
this.store = new Store(props.keyField);
this.store.data = props.data;
this.wrapComponents();
this.handleUpdateCell = this.handleUpdateCell.bind(this);
}
componentWillReceiveProps(nextProps) {
this.store.data = nextProps.data;
this.store.setAllData(nextProps.data);
}
wrapComponents() {
@ -45,41 +44,13 @@ const withDataStore = Base =>
});
}
if (cellEdit) {
this.BaseComponent = withCellEdit(this.BaseComponent);
}
if (selectRow) {
this.BaseComponent = withSelection(this.BaseComponent);
}
if (cellEdit) {
this.BaseComponent = withCellEdit(this.BaseComponent, {
ref: node => this.cellEditWrapper = node,
onUpdateCell: this.handleUpdateCell
});
}
}
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.cellEditWrapper.completeEditing();
}
}).catch((e) => {
this.cellEditWrapper.updateEditingWithErr(e.message);
});
}
return false;
}
render() {

View File

@ -10,7 +10,7 @@ export default ExtendBase =>
filters: store.filters,
sortField: store.sortField,
sortOrder: store.sortOrder,
data: store.data,
data: store.getAllData(),
...state
};
}
@ -30,6 +30,11 @@ export default ExtendBase =>
return remote === true || (_.isObject(remote) && remote.sort);
}
isRemoteCellEdit() {
const { remote } = this.props;
return remote === true || (_.isObject(remote) && remote.cellEdit);
}
handleRemotePageChange() {
this.props.onTableChange('pagination', this.getNewestState());
}
@ -46,4 +51,9 @@ export default ExtendBase =>
handleSortChange() {
this.props.onTableChange('sort', this.getNewestState());
}
handleCellChange(rowId, dataField, newValue) {
const cellEdit = { rowId, dataField, newValue };
this.props.onTableChange('cellEdit', this.getNewestState({ cellEdit }));
}
};

View File

@ -34,6 +34,10 @@ export default class Store {
return this._data;
}
setAllData(data) {
this._data = data;
}
get data() {
if (Object.keys(this._filters).length > 0) {
return this._filteredData;

View File

@ -31,13 +31,10 @@ describe('CellEditWrapper', () => {
};
const keyField = 'id';
let onUpdateCellCB = sinon.stub();
const store = new Store(keyField);
store.data = data;
const CellEditWrapper = wrapperFactory(Container, {
onUpdateCell: onUpdateCellCB
});
const CellEditWrapper = wrapperFactory(Container);
beforeEach(() => {
wrapper = shallow(
@ -60,24 +57,24 @@ describe('CellEditWrapper', () => {
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().message).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
expect(wrapper.state().isDataChanged).toBeFalsy();
});
it('should inject correct props to base component', () => {
expect(wrapper.props().onCellUpdate).toBeDefined();
expect(wrapper.props().onStartEditing).toBeDefined();
expect(wrapper.props().onEscapeEditing).toBeDefined();
expect(wrapper.props().isDataChanged).toBe(wrapper.state().isDataChanged);
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', () => {
describe('and cellEdit is not work on remote', () => {
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
@ -88,54 +85,69 @@ describe('CellEditWrapper', () => {
store={ store }
/>
);
wrapper.setProps({ cellEdit: { ...cellEdit, editing: false } });
wrapper.setProps({ cellEdit: { ...cellEdit } });
});
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();
it('should always setting state.isDataChanged as false', () => {
expect(wrapper.state().isDataChanged).toBeFalsy();
});
});
describe('and cellEdit.editing is true', () => {
const errorMessage = 'test';
describe('and cellEdit is work on remote', () => {
let errorMessage;
const ridx = 1;
const cidx = 2;
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
store={ store }
/>
);
wrapper.setState({ ridx, cidx, editing: true });
wrapper.setProps({ cellEdit: { ...cellEdit, editing: true, errorMessage } });
describe('and cellEdit.errorMessage is defined', () => {
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
remote={ { cellEdit: true } }
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
store={ store }
/>
);
errorMessage = 'test';
wrapper.setState({ ridx, cidx });
wrapper.setProps({ cellEdit: { ...cellEdit, errorMessage } });
});
it('should setting correct state', () => {
expect(wrapper.state().ridx).toEqual(ridx);
expect(wrapper.state().cidx).toEqual(cidx);
expect(wrapper.state().isDataChanged).toBeFalsy();
expect(wrapper.state().message).toEqual(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('and cellEdit.errorMessage is undefined', () => {
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
remote={ { cellEdit: true } }
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
store={ store }
/>
);
errorMessage = null;
wrapper.setState({ ridx, cidx });
wrapper.setProps({ cellEdit: { ...cellEdit, errorMessage } });
});
describe('call updateEditingWithErr function', () => {
it('should set state.message correctly', () => {
const message = 'test';
wrapper.instance().updateEditingWithErr(message);
expect(wrapper.state().message).toEqual(message);
it('should setting correct state', () => {
expect(wrapper.state().isDataChanged).toBeTruthy();
});
it('should escape current editing', () => {
expect(spy).toHaveBeenCalled();
});
});
});
});
@ -144,7 +156,6 @@ describe('CellEditWrapper', () => {
wrapper.instance().escapeEditing();
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
});
});
@ -155,7 +166,7 @@ describe('CellEditWrapper', () => {
wrapper.instance().startEditing(ridx, cidx);
expect(wrapper.state().ridx).toEqual(ridx);
expect(wrapper.state().cidx).toEqual(cidx);
expect(wrapper.state().editing).toBeTruthy();
expect(wrapper.state().isDataChanged).toBeFalsy();
});
describe('if selectRow.clickToSelect is defined', () => {
@ -199,7 +210,6 @@ describe('CellEditWrapper', () => {
wrapper.instance().startEditing(ridx, cidx);
expect(wrapper.state().ridx).toEqual(ridx);
expect(wrapper.state().cidx).toEqual(cidx);
expect(wrapper.state().editing).toBeTruthy();
});
});
});
@ -210,7 +220,7 @@ describe('CellEditWrapper', () => {
expect(wrapper.state().ridx).toBeNull();
expect(wrapper.state().cidx).toBeNull();
expect(wrapper.state().message).toBeNull();
expect(wrapper.state().editing).toBeFalsy();
expect(wrapper.state().isDataChanged).toBeTruthy();
});
});
@ -219,35 +229,75 @@ describe('CellEditWrapper', () => {
const column = columns[1];
const newValue = 'new name';
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
store={ store }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
describe('when cell edit is work on remote', () => {
const spy = jest.spyOn(CellEditWrapper.prototype, 'handleCellChange');
const onTableChangeCB = jest.fn();
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
remote={ { cellEdit: true } }
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
onTableChange={ onTableChangeCB }
store={ store }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
});
it('should calling handleCellChange correctly', () => {
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls).toHaveLength(1);
expect(spy.mock.calls[0]).toHaveLength(3);
expect(spy.mock.calls[0][0]).toEqual(row[keyField]);
expect(spy.mock.calls[0][1]).toEqual(column.dataField);
expect(spy.mock.calls[0][2]).toEqual(newValue);
});
});
it('should calling onUpdateCell callback correctly', () => {
expect(onUpdateCellCB.callCount).toBe(1);
expect(onUpdateCellCB.calledWith(row.id, column.dataField, newValue)).toBe(true);
});
describe('when cell edit is not work on remote', () => {
const spyOnCompleteEditing = jest.spyOn(CellEditWrapper.prototype, 'completeEditing');
const spyOnStoreEdit = jest.spyOn(Store.prototype, 'edit');
describe('when onUpdateCell function return true', () => {
const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing');
beforeEach(() => {
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
store={ store }
/>
);
wrapper.instance().handleCellUpdate(row, column, newValue);
});
afterEach(() => {
spyOnStoreEdit.mockReset();
spyOnCompleteEditing.mockReset();
});
it('should calling props.store.edit', () => {
expect(spyOnStoreEdit).toHaveBeenCalled();
expect(spyOnStoreEdit.mock.calls).toHaveLength(1);
expect(spyOnStoreEdit.mock.calls[0]).toHaveLength(3);
expect(spyOnStoreEdit.mock.calls[0][0]).toEqual(row[keyField]);
expect(spyOnStoreEdit.mock.calls[0][1]).toEqual(column.dataField);
expect(spyOnStoreEdit.mock.calls[0][2]).toEqual(newValue);
});
it('should calling completeEditing function', () => {
expect(spy).toHaveBeenCalled();
expect(spyOnCompleteEditing).toHaveBeenCalled();
});
describe('if cellEdit.afterSaveCell prop defined', () => {
const aftereSaveCellCallBack = sinon.stub();
beforeEach(() => {
cellEdit.beforeSaveCell = aftereSaveCellCallBack;
cellEdit.afterSaveCell = aftereSaveCellCallBack;
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
@ -269,28 +319,6 @@ describe('CellEditWrapper', () => {
});
});
describe('when onUpdateCell function return false', () => {
const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing');
beforeEach(() => {
onUpdateCellCB = sinon.stub().returns(false);
wrapper = shallow(
<CellEditWrapper
keyField={ keyField }
data={ data }
columns={ columns }
cellEdit={ cellEdit }
store={ store }
/>
);
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(() => {

View File

@ -1,12 +1,10 @@
/* eslint react/prefer-stateless-function: 0 */
/* eslint react/no-multi-comp: 0 */
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import BootstrapTable from '../src/bootstrap-table';
import Container from '../src';
import { getRowByRowId } from '../src/store/rows';
describe('container', () => {
let wrapper;
@ -79,63 +77,6 @@ describe('container', () => {
it('should render BootstrapTable component successfully', () => {
expect(wrapper.dive().find(BootstrapTable)).toHaveLength(1);
});
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 = getRowByRowId(store)(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(
<Container
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 = getRowByRowId(store)(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', () => {});
});
});
describe('when selectRow prop is defined', () => {

View File

@ -93,7 +93,6 @@ describe('TableResolver', () => {
const expectNonEditableRows = [1, 2];
const cellEdit = {
mode: Const.DBCLICK_TO_CELL_EDIT,
onUpdate: sinon.stub(),
blurToSave: true,
beforeSaveCell: sinon.stub(),
afterSaveCell: sinon.stub(),

View File

@ -134,6 +134,58 @@ describe('remoteResolver', () => {
});
});
describe('isRemoteCellEdit', () => {
describe('when remote is false', () => {
beforeEach(() => {
shallowContainer();
});
it('should return false', () => {
expect(wrapper.instance().isRemoteCellEdit()).toBeFalsy();
});
});
describe('when remote is true', () => {
beforeEach(() => {
shallowContainer({ remote: true });
});
it('should return true', () => {
expect(wrapper.instance().isRemoteCellEdit()).toBeTruthy();
});
});
describe('when remote.cellEdit is true', () => {
beforeEach(() => {
shallowContainer({ remote: { cellEdit: true } });
});
it('should return true', () => {
expect(wrapper.instance().isRemoteCellEdit()).toBeTruthy();
});
});
});
describe('handleCellChange', () => {
const onTableChangeCB = sinon.stub();
const rowId = 1;
const dataField = 'name';
const newValue = 'test';
beforeEach(() => {
onTableChangeCB.reset();
shallowContainer({ onTableChange: onTableChangeCB });
wrapper.instance().handleCellChange(rowId, dataField, newValue);
});
it('should calling props.onTableChange correctly', () => {
const cellEdit = { rowId, dataField, newValue };
expect(onTableChangeCB.calledOnce).toBeTruthy();
expect(onTableChangeCB.calledWith(
'cellEdit', wrapper.instance().getNewestState({ cellEdit }))).toBeTruthy();
});
});
describe('handleSortChange', () => {
const onTableChangeCB = sinon.stub();
beforeEach(() => {

View File

@ -273,7 +273,6 @@ describe('Row', () => {
cellEdit.cidx = editingColIndex;
cellEdit.onUpdate = sinon.stub();
cellEdit.onEscape = sinon.stub();
cellEdit.onUpdate = sinon.stub();
wrapper = shallow(
<Row
{ ...mockBodyResolvedProps }