diff --git a/docs/README.md b/docs/README.md
index 19330f8..3b7311f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -42,6 +42,7 @@ Following is a `cellEdit` object:
{
mode: 'click',
blurToSave: true,
+ timeToCloseMessage: 2500,
onEditing: (rowId, dataField, newValue) => { ... },
beforeSaveCell: (oldValue, newValue, row, column) => { ... },
afterSaveCell: (oldValue, newValue, row, column) => { ... },
@@ -49,10 +50,13 @@ Following is a `cellEdit` object:
}
```
#### cellEdit.mode - [String]
-`cellEdit.mode` possible value is `click` and `dbclick`. It's required value that tell `react-bootstrap-table2` how to trigger the cell editing.
+`cellEdit.mode` possible value is `click` and `dbclick`. **It's required value** that tell `react-bootstrap-table2` how to trigger the cell editing.
#### cellEdit.blurToSave - [Bool]
Default is `false`, enable it will be able to save the cell automatically when blur from the cell editor.
#### cellEdit.nonEditableRows - [Function]
`cellEdit.nonEditableRows` accept a callback function and expect return an array which used to restrict all the columns of some rows as non-editable. So the each item in return array should be rowkey(`keyField`)
+
+#### cellEdit.timeToCloseMessage - [Function]
+If a [`column.validator`](./columns.md#validator) defined and the new value is invalid, `react-bootstrap-table2` will popup a alert at the bottom of editor. `cellEdit.timeToCloseMessage` is a chance to let you decide how long the alert should be stay. Default is 3000 millisecond.
\ No newline at end of file
diff --git a/docs/columns.md b/docs/columns.md
index 8ac781e..dc1e378 100644
--- a/docs/columns.md
+++ b/docs/columns.md
@@ -26,6 +26,7 @@ Available properties in a column object:
* [headerAlign](#headerAlign)
* [headerAttrs](#headerAttrs)
* [editable](#editable)
+* [validator](#validator)
Following is a most simplest and basic usage:
@@ -417,4 +418,25 @@ A new `Object` will be the result of element headerAttrs.
> overwrited when other props related to HTML attributes were given.
## column.editable - [Bool]
-`column.editable` default is true, means every column is editable if you configure [`cellEdit`](./README.md#cellEdit). But you can disable some columns editable via setting `false`.
\ No newline at end of file
+`column.editable` default is true, means every column is editable if you configure [`cellEdit`](./README.md#cellEdit). But you can disable some columns editable via setting `false`.
+
+## column.validator - [Function]
+`column.validator` used for validate the data when cell on updating. it's should accept a callback function with following argument:
+`newValue`, `row` and `column`:
+
+```js
+{
+ // omit...
+ validator: (newValue, row, column) => {
+ return ...;
+ }
+}
+```
+
+The return value can be a bool or an object. If your valiation is pass, return `true` explicitly. If your valiation is invalid, return following object instead:
+```js
+{
+ valid: false,
+ message: 'SOME_REASON_HERE'
+}
+```
\ No newline at end of file
diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js
new file mode 100644
index 0000000..d3dcffb
--- /dev/null
+++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js
@@ -0,0 +1,85 @@
+import React from 'react';
+/* eslint no-unused-vars: 0 */
+import { BootstrapTableful } from 'react-bootstrap-table2';
+import Code from 'components/common/code-block';
+import { productsGenerator } from 'utils/common';
+
+const products = productsGenerator();
+
+const columns = [{
+ dataField: 'id',
+ text: 'Product ID'
+}, {
+ dataField: 'name',
+ text: 'Product Name'
+}, {
+ dataField: 'price',
+ text: 'Product Price',
+ validator: (newValue, row, column) => {
+ if (isNaN(newValue)) {
+ return {
+ valid: false,
+ message: 'Price should be numeric'
+ };
+ }
+ if (newValue < 2000) {
+ return {
+ valid: false,
+ message: 'Price should bigger than 2000'
+ };
+ }
+ return true;
+ }
+}];
+
+const sourceCode = `\
+const columns = [{
+ dataField: 'id',
+ text: 'Product ID'
+}, {
+ dataField: 'name',
+ text: 'Product Name'
+}, {
+ dataField: 'price',
+ text: 'Product Price',
+ validator: (newValue, row, column) => {
+ if (isNaN(newValue)) {
+ return {
+ valid: false,
+ message: 'Price should be numeric'
+ };
+ }
+ if (newValue < 2000) {
+ return {
+ valid: false,
+ message: 'Price should bigger than 2000'
+ };
+ }
+ return true;
+ }
+}];
+
+const cellEdit = {
+ mode: 'click',
+ blurToSave: true
+};
+
+
+`;
+
+const cellEdit = {
+ mode: 'click',
+ blurToSave: true
+};
+export default () => (
+
+
Product Price should bigger than $2000
+
+ { sourceCode }
+
+);
diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js
index 9ad48f2..7c43434 100644
--- a/packages/react-bootstrap-table2-example/stories/index.js
+++ b/packages/react-bootstrap-table2-example/stories/index.js
@@ -42,6 +42,7 @@ import BlurToSaveTable from 'examples/cell-edit/blur-to-save-table';
import RowLevelEditableTable from 'examples/cell-edit/row-level-editable-table';
import ColumnLevelEditableTable from 'examples/cell-edit/column-level-editable-table';
import CellEditHooks from 'examples/cell-edit/cell-edit-hooks-table';
+import CellEditValidator from 'examples/cell-edit/cell-edit-validator-table';
// css style
import 'bootstrap/dist/css/bootstrap.min.css';
@@ -92,4 +93,5 @@ storiesOf('Cell Editing', module)
.add('Blur to Save Cell', () => )
.add('Row Level Editable', () => )
.add('Column Level Editable', () => )
- .add('Rich Hook Functions', () => );
+ .add('Rich Hook Functions', () => )
+ .add('Validation', () => );
diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js
index d7ea363..f93a30a 100644
--- a/packages/react-bootstrap-table2/src/bootstrap-table.js
+++ b/packages/react-bootstrap-table2/src/bootstrap-table.js
@@ -137,7 +137,8 @@ BootstrapTable.propTypes = {
blurToSave: PropTypes.bool,
beforeSaveCell: PropTypes.func,
afterSaveCell: PropTypes.func,
- nonEditableRows: PropTypes.func
+ nonEditableRows: PropTypes.func,
+ timeToCloseMessage: PropTypes.number
})
};
diff --git a/packages/react-bootstrap-table2/src/const.js b/packages/react-bootstrap-table2/src/const.js
index 8a812ee..5391b82 100644
--- a/packages/react-bootstrap-table2/src/const.js
+++ b/packages/react-bootstrap-table2/src/const.js
@@ -3,5 +3,6 @@ export default {
SORT_DESC: 'desc',
UNABLE_TO_CELL_EDIT: 'none',
CLICK_TO_CELL_EDIT: 'click',
- DBCLICK_TO_CELL_EDIT: 'dbclick'
+ DBCLICK_TO_CELL_EDIT: 'dbclick',
+ TIME_TO_CLOSE_MESSAGE: 3000
};
diff --git a/packages/react-bootstrap-table2/src/editing-cell.js b/packages/react-bootstrap-table2/src/editing-cell.js
index f32e1ad..97320c9 100644
--- a/packages/react-bootstrap-table2/src/editing-cell.js
+++ b/packages/react-bootstrap-table2/src/editing-cell.js
@@ -1,33 +1,73 @@
+/* eslint arrow-body-style: 0 */
/* eslint react/prop-types: 0 */
/* eslint no-return-assign: 0 */
import React, { Component } from 'react';
+import cs from 'classnames';
import PropTypes from 'prop-types';
import _ from './utils';
+import Const from './const';
import TextEditor from './text-editor';
+import EditorIndicator from './editor-indicator';
class EditingCell extends Component {
constructor(props) {
super(props);
+ this.indicatorTimer = null;
+ this.clearTimer = this.clearTimer.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.beforeComplete = this.beforeComplete.bind(this);
+ this.state = {
+ invalidMessage: null
+ };
+ }
+
+ componentWillUnmount() {
+ this.clearTimer();
+ }
+
+ clearTimer() {
+ if (this.indicatorTimer) {
+ clearTimeout(this.indicatorTimer);
+ }
+ }
+
+ beforeComplete(row, column, newValue) {
+ this.clearTimer();
+ const { onComplete, timeToCloseMessage } = 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);
+ return;
+ }
+ }
+ onComplete(row, column, newValue);
}
handleBlur() {
- const { onEscape, onComplete, blurToSave, row, column } = this.props;
+ const { onEscape, blurToSave, row, column } = this.props;
if (blurToSave) {
const value = this.editor.text.value;
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
- onComplete(row, column, value);
+ this.beforeComplete(row, column, value);
} else {
onEscape();
}
}
handleKeyDown(e) {
- const { onEscape, onComplete, row, column } = this.props;
+ const { onEscape, row, column } = this.props;
if (e.keyCode === 27) { // ESC
onEscape();
} else if (e.keyCode === 13) { // ENTER
@@ -35,11 +75,12 @@ class EditingCell extends Component {
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
- onComplete(row, column, value);
+ this.beforeComplete(row, column, value);
}
}
render() {
+ const { invalidMessage } = this.state;
const { row, column } = this.props;
const { dataField } = column;
@@ -48,9 +89,17 @@ class EditingCell extends Component {
onKeyDown: this.handleKeyDown,
onBlur: this.handleBlur
};
+
+ const editorClass = invalidMessage ? cs('animated', 'shake') : null;
return (
-
- this.editor = node } defaultValue={ value } { ...editorAttrs } />
+
+ this.editor = node }
+ defaultValue={ value }
+ classNames={ editorClass }
+ { ...editorAttrs }
+ />
+ { invalidMessage ? : null }
);
}
@@ -60,7 +109,12 @@ EditingCell.propTypes = {
row: PropTypes.object.isRequired,
column: PropTypes.object.isRequired,
onComplete: PropTypes.func.isRequired,
- onEscape: PropTypes.func.isRequired
+ onEscape: PropTypes.func.isRequired,
+ timeToCloseMessage: PropTypes.number
+};
+
+EditingCell.defaultProps = {
+ timeToCloseMessage: Const.TIME_TO_CLOSE_MESSAGE
};
export default EditingCell;
diff --git a/packages/react-bootstrap-table2/src/editor-indicator.js b/packages/react-bootstrap-table2/src/editor-indicator.js
new file mode 100644
index 0000000..c19819d
--- /dev/null
+++ b/packages/react-bootstrap-table2/src/editor-indicator.js
@@ -0,0 +1,19 @@
+/* eslint no-return-assign: 0 */
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const EditorIndicator = ({ invalidMessage }) =>
+ (
+
+ { invalidMessage }
+
+ );
+
+EditorIndicator.propTypes = {
+ invalidMessage: PropTypes.string
+};
+
+EditorIndicator.defaultProps = {
+ invalidMessage: null
+};
+export default EditorIndicator;
diff --git a/packages/react-bootstrap-table2/src/header-cell.js b/packages/react-bootstrap-table2/src/header-cell.js
index d419710..1b9c42a 100644
--- a/packages/react-bootstrap-table2/src/header-cell.js
+++ b/packages/react-bootstrap-table2/src/header-cell.js
@@ -99,7 +99,9 @@ HeaderCell.propTypes = {
headerAlign: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
align: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
sort: PropTypes.bool,
- sortFunc: PropTypes.func
+ sortFunc: PropTypes.func,
+ editable: PropTypes.bool,
+ validator: PropTypes.func
}).isRequired,
index: PropTypes.number.isRequired,
onSort: PropTypes.func,
diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js
index c0e06b2..ed76102 100644
--- a/packages/react-bootstrap-table2/src/row.js
+++ b/packages/react-bootstrap-table2/src/row.js
@@ -16,13 +16,11 @@ const Row = (props) => {
editable: editableRow
} = props;
const {
- ridx: editingRowIdx,
- cidx: editingColIdx,
mode,
onStart,
- onEscape,
- onComplete,
- blurToSave
+ ridx: editingRowIdx,
+ cidx: editingColIdx,
+ ...rest
} = cellEdit;
return (
@@ -36,9 +34,7 @@ const Row = (props) => {
key={ _.get(row, column.dataField) }
row={ row }
column={ column }
- blurToSave={ blurToSave }
- onComplete={ onComplete }
- onEscape={ onEscape }
+ { ...rest }
/>
);
}
diff --git a/packages/react-bootstrap-table2/src/text-editor.js b/packages/react-bootstrap-table2/src/text-editor.js
index 285236d..4c41954 100644
--- a/packages/react-bootstrap-table2/src/text-editor.js
+++ b/packages/react-bootstrap-table2/src/text-editor.js
@@ -1,5 +1,6 @@
/* eslint no-return-assign: 0 */
import React, { Component } from 'react';
+import cs from 'classnames';
import PropTypes from 'prop-types';
class TextEditor extends Component {
@@ -10,12 +11,13 @@ class TextEditor extends Component {
}
render() {
- const { defaultValue, ...rest } = this.props;
+ const { defaultValue, classNames, ...rest } = this.props;
+ const editorClass = cs('form-control editor edit-text', classNames);
return (
this.text = node }
type="text"
- className="form-control editor edit-text"
+ className={ editorClass }
{ ...rest }
/>
);
@@ -23,9 +25,16 @@ class TextEditor extends Component {
}
TextEditor.propTypes = {
+ classNames: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.object
+ ]),
defaultValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired
};
+TextEditor.defaultProps = {
+ classNames: null
+};
export default TextEditor;
diff --git a/packages/react-bootstrap-table2/style/react-bootstrap-table.scss b/packages/react-bootstrap-table2/style/react-bootstrap-table.scss
index 91cc609..9720bd0 100644
--- a/packages/react-bootstrap-table2/style/react-bootstrap-table.scss
+++ b/packages/react-bootstrap-table2/style/react-bootstrap-table.scss
@@ -25,4 +25,94 @@
td.react-bs-table-no-data {
text-align: center;
}
+
+ td.react-bootstrap-table-editing-cell {
+ .animated {
+ animation-fill-mode: both;
+ }
+
+ .animated.bounceIn,
+ .animated.bounceOut{
+ animation-duration: .75s;
+ }
+
+ .animated.shake{
+ animation-duration: .3s;
+ }
+
+ @keyframes shake {
+ from, to {
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%, 50%, 90% {
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 30%, 70%{
+ transform: translate3d(10px, 0, 0);
+ }
+ }
+
+ .shake {
+ animation-name: shake;
+ }
+
+ @keyframes bounceIn {
+ from, 20%, 40%, 60%, 80%, to {
+ animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 20% {
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 40% {
+ transform: scale3d(.9, .9, .9);
+ }
+
+ 60% {
+ opacity: 1;
+ transform: scale3d(1.03, 1.03, 1.03);
+ }
+
+ 80% {
+ transform: scale3d(.97, .97, .97);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale3d(1, 1, 1);
+ }
+ }
+
+ .bounceIn {
+ animation-name: bounceIn;
+ }
+
+ @keyframes bounceOut {
+ 20% {
+ transform: scale3d(.9, .9, .9);
+ }
+
+ 50%, 55% {
+ opacity: 1;
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ to {
+ opacity: 0;
+ transform: scale3d(.3, .3, .3);
+ }
+ }
+
+ .bounceOut {
+ animation-name: bounceOut;
+ }
+ }
}
\ No newline at end of file
diff --git a/packages/react-bootstrap-table2/test/editing-cell.test.js b/packages/react-bootstrap-table2/test/editing-cell.test.js
index 9bf0f19..835f7aa 100644
--- a/packages/react-bootstrap-table2/test/editing-cell.test.js
+++ b/packages/react-bootstrap-table2/test/editing-cell.test.js
@@ -6,6 +6,7 @@ import { shallow, mount } from 'enzyme';
import { TableRowWrapper } from './test-helpers/table-wrapper';
import EditingCell from '../src/editing-cell';
import TextEditor from '../src/text-editor';
+import EditorIndicator from '../src/editor-indicator';
describe('EditingCell', () => {
@@ -17,7 +18,7 @@ describe('EditingCell', () => {
name: 'A'
};
- const column = {
+ let column = {
dataField: 'id',
text: 'ID'
};
@@ -39,6 +40,7 @@ describe('EditingCell', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('td').length).toBe(1);
expect(wrapper.find(TextEditor).length).toBe(1);
+ expect(wrapper.state().invalidMessage).toBeNull();
});
it('should render TextEditor with correct props', () => {
@@ -46,6 +48,12 @@ describe('EditingCell', () => {
expect(textEditor.props().defaultValue).toEqual(row[column.dataField]);
expect(textEditor.props().onKeyDown).toBeDefined();
expect(textEditor.props().onBlur).toBeDefined();
+ expect(textEditor.props().classNames).toBeNull();
+ });
+
+ it('should not render EditorIndicator due to state.invalidMessage is null', () => {
+ const indicator = wrapper.find(EditorIndicator);
+ expect(indicator.length).toEqual(0);
});
it('when press ENTER on TextEditor should call onComplete correctly', () => {
@@ -90,4 +98,76 @@ describe('EditingCell', () => {
expect(onComplete.calledWith(row, column, `${row[column.dataField]}`)).toBe(true);
});
});
+
+ describe('when column.validator is defined', () => {
+ let newValue;
+ let validForm;
+ let validatorCallBack;
+
+ describe('and column.validator return an object', () => {
+ beforeEach(() => {
+ newValue = 'newValue';
+ validForm = { valid: false, message: 'Something is invalid' };
+ validatorCallBack = sinon.stub().returns(validForm);
+ column = {
+ dataField: 'id',
+ text: 'ID',
+ validator: validatorCallBack
+ };
+ wrapper.instance().beforeComplete(row, column, newValue);
+ });
+
+ it('should call column.validator successfully', () => {
+ expect(validatorCallBack.callCount).toBe(1);
+ expect(validatorCallBack.calledWith(newValue, row, column)).toBe(true);
+ });
+
+ it('should not call onComplete', () => {
+ expect(onComplete.callCount).toBe(0);
+ });
+
+ it('should set indicatorTimer successfully', () => {
+ expect(wrapper.instance().indicatorTimer).toBeDefined();
+ });
+
+ it('should set invalidMessage state correctly', () => {
+ expect(wrapper.state().invalidMessage).toEqual(validForm.message);
+ });
+
+ it('should render TextEditor with correct shake and animated class', () => {
+ const editor = wrapper.find(TextEditor);
+ expect(editor.length).toEqual(1);
+ expect(editor.props().classNames).toEqual('animated shake');
+ });
+
+ it('should render EditorIndicator correctly', () => {
+ const indicator = wrapper.find(EditorIndicator);
+ expect(indicator.length).toEqual(1);
+ expect(indicator.props().invalidMessage).toEqual(validForm.message);
+ });
+ });
+
+ describe('and column.validator return true or something', () => {
+ beforeEach(() => {
+ newValue = 'newValue';
+ validForm = true;
+ validatorCallBack = sinon.stub().returns(validForm);
+ column = {
+ dataField: 'id',
+ text: 'ID',
+ validator: validatorCallBack
+ };
+ wrapper.instance().beforeComplete(row, column, newValue);
+ });
+
+ it('should call column.validator successfully', () => {
+ expect(validatorCallBack.callCount).toBe(1);
+ expect(validatorCallBack.calledWith(newValue, row, column)).toBe(true);
+ });
+
+ it('should call onComplete', () => {
+ expect(onComplete.callCount).toBe(1);
+ });
+ });
+ });
});
diff --git a/packages/react-bootstrap-table2/test/text-editor.test.js b/packages/react-bootstrap-table2/test/text-editor.test.js
index dbdaeef..0150bc3 100644
--- a/packages/react-bootstrap-table2/test/text-editor.test.js
+++ b/packages/react-bootstrap-table2/test/text-editor.test.js
@@ -21,4 +21,21 @@ describe('TextEditor', () => {
expect(wrapper.find('input').prop('type')).toEqual('text');
expect(wrapper.find('.form-control.editor.edit-text').length).toBe(1);
});
+
+ describe('whenclassNames prop defined', () => {
+ const className = 'test-class';
+ beforeEach(() => {
+ wrapper = shallow(
+
+ );
+ });
+
+ it('should render correct custom classname', () => {
+ expect(wrapper.length).toBe(1);
+ expect(wrapper.find(`.${className}`).length).toBe(1);
+ });
+ });
});