Skip to content

Commit 55fe007

Browse files
authored
Merge pull request #165 from react-bootstrap-table/enhance/cell-edit-as-package
react-bootstrap-table2-editor
2 parents 6913434 + 618346b commit 55fe007

39 files changed

+831
-843
lines changed

docs/cell-edit.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Cell Editing
2+
Before start to use cell edit, please remember to install `react-bootstrap-table2-editor`
3+
4+
```sh
5+
$ npm install react-bootstrap-table2-editor --save
6+
```
7+
18
# Properties on cellEdit prop
29
* [mode (**required**)](#mode)
310
* [blurToSave](#blurToSave)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "react-bootstrap-table2-editor",
3+
"version": "0.0.1",
4+
"description": "it's the editor addon for react-bootstrap-table2",
5+
"main": "src/index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC"
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const TIME_TO_CLOSE_MESSAGE = 3000;
2+
export const DELAY_FOR_DBCLICK = 200;
3+
export const CLICK_TO_CELL_EDIT = 'click';
4+
export const DBCLICK_TO_CELL_EDIT = 'dbclick';
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/* eslint react/prop-types: 0 */
2+
/* eslint no-return-assign: 0 */
3+
/* eslint class-methods-use-this: 0 */
4+
/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */
5+
import React, { Component } from 'react';
6+
import cs from 'classnames';
7+
import PropTypes from 'prop-types';
8+
9+
import TextEditor from './text-editor';
10+
import EditorIndicator from './editor-indicator';
11+
import { TIME_TO_CLOSE_MESSAGE } from './const';
12+
13+
export default _ =>
14+
class EditingCell extends Component {
15+
static propTypes = {
16+
row: PropTypes.object.isRequired,
17+
column: PropTypes.object.isRequired,
18+
onUpdate: PropTypes.func.isRequired,
19+
onEscape: PropTypes.func.isRequired,
20+
timeToCloseMessage: PropTypes.number,
21+
className: PropTypes.string,
22+
style: PropTypes.object
23+
}
24+
25+
static defaultProps = {
26+
timeToCloseMessage: TIME_TO_CLOSE_MESSAGE,
27+
className: null,
28+
style: {}
29+
}
30+
31+
constructor(props) {
32+
super(props);
33+
this.indicatorTimer = null;
34+
this.clearTimer = this.clearTimer.bind(this);
35+
this.handleBlur = this.handleBlur.bind(this);
36+
this.handleClick = this.handleClick.bind(this);
37+
this.handleKeyDown = this.handleKeyDown.bind(this);
38+
this.beforeComplete = this.beforeComplete.bind(this);
39+
this.state = {
40+
invalidMessage: null
41+
};
42+
}
43+
44+
componentWillReceiveProps({ message }) {
45+
if (_.isDefined(message)) {
46+
this.createTimer();
47+
this.setState(() => ({
48+
invalidMessage: message
49+
}));
50+
}
51+
}
52+
53+
componentWillUnmount() {
54+
this.clearTimer();
55+
}
56+
57+
clearTimer() {
58+
if (this.indicatorTimer) {
59+
clearTimeout(this.indicatorTimer);
60+
}
61+
}
62+
63+
createTimer() {
64+
this.clearTimer();
65+
const { timeToCloseMessage, onErrorMessageDisappear } = this.props;
66+
this.indicatorTimer = _.sleep(() => {
67+
this.setState(() => ({
68+
invalidMessage: null
69+
}));
70+
if (_.isFunction(onErrorMessageDisappear)) onErrorMessageDisappear();
71+
}, timeToCloseMessage);
72+
}
73+
74+
beforeComplete(row, column, newValue) {
75+
const { onUpdate } = this.props;
76+
if (_.isFunction(column.validator)) {
77+
const validateForm = column.validator(newValue, row, column);
78+
if (_.isObject(validateForm) && !validateForm.valid) {
79+
this.setState(() => ({
80+
invalidMessage: validateForm.message
81+
}));
82+
this.createTimer();
83+
return;
84+
}
85+
}
86+
onUpdate(row, column, newValue);
87+
}
88+
89+
handleBlur() {
90+
const { onEscape, blurToSave, row, column } = this.props;
91+
if (blurToSave) {
92+
const value = this.editor.text.value;
93+
if (!_.isDefined(value)) {
94+
// TODO: for other custom or embed editor
95+
}
96+
this.beforeComplete(row, column, value);
97+
} else {
98+
onEscape();
99+
}
100+
}
101+
102+
handleKeyDown(e) {
103+
const { onEscape, row, column } = this.props;
104+
if (e.keyCode === 27) { // ESC
105+
onEscape();
106+
} else if (e.keyCode === 13) { // ENTER
107+
const value = e.currentTarget.value;
108+
if (!_.isDefined(value)) {
109+
// TODO: for other custom or embed editor
110+
}
111+
this.beforeComplete(row, column, value);
112+
}
113+
}
114+
115+
handleClick(e) {
116+
if (e.target.tagName !== 'TD') {
117+
// To avoid the row selection event be triggered,
118+
// When user define selectRow.clickToSelect and selectRow.clickToEdit
119+
// We shouldn't trigger selection event even if user click on the cell editor(input)
120+
e.stopPropagation();
121+
}
122+
}
123+
124+
render() {
125+
const { invalidMessage } = this.state;
126+
const { row, column, className, style } = this.props;
127+
const { dataField } = column;
128+
129+
const value = _.get(row, dataField);
130+
const editorAttrs = {
131+
onKeyDown: this.handleKeyDown,
132+
onBlur: this.handleBlur
133+
};
134+
135+
const hasError = _.isDefined(invalidMessage);
136+
const editorClass = hasError ? cs('animated', 'shake') : null;
137+
return (
138+
<td
139+
className={ cs('react-bootstrap-table-editing-cell', className) }
140+
style={ style }
141+
onClick={ this.handleClick }
142+
>
143+
<TextEditor
144+
ref={ node => this.editor = node }
145+
defaultValue={ value }
146+
className={ editorClass }
147+
{ ...editorAttrs }
148+
/>
149+
{ hasError ? <EditorIndicator invalidMessage={ invalidMessage } /> : null }
150+
</td>
151+
);
152+
}
153+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import wrapperFactory from './wrapper';
2+
import editingCellFactory from './editing-cell';
3+
import {
4+
CLICK_TO_CELL_EDIT,
5+
DBCLICK_TO_CELL_EDIT,
6+
DELAY_FOR_DBCLICK
7+
} from './const';
8+
9+
export default (options = {}) => ({
10+
wrapperFactory,
11+
editingCellFactory,
12+
CLICK_TO_CELL_EDIT,
13+
DBCLICK_TO_CELL_EDIT,
14+
DELAY_FOR_DBCLICK,
15+
options
16+
});

packages/react-bootstrap-table2/src/cell-edit/wrapper.js renamed to packages/react-bootstrap-table2-editor/src/wrapper.js

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
11
/* eslint react/prop-types: 0 */
22
import React, { Component } from 'react';
3-
import _ from '../utils';
4-
import remoteResolver from '../props-resolver/remote-resolver';
3+
import PropTypes from 'prop-types';
4+
5+
import { CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT } from './const';
6+
7+
export default (
8+
Base,
9+
{ _, remoteResolver }
10+
) => {
11+
let EditingCell;
12+
return class CellEditWrapper extends remoteResolver(Component) {
13+
static propTypes = {
14+
options: PropTypes.shape({
15+
mode: PropTypes.oneOf([CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT]).isRequired,
16+
onErrorMessageDisappear: PropTypes.func,
17+
blurToSave: PropTypes.bool,
18+
beforeSaveCell: PropTypes.func,
19+
afterSaveCell: PropTypes.func,
20+
nonEditableRows: PropTypes.func,
21+
timeToCloseMessage: PropTypes.number,
22+
errorMessage: PropTypes.string
23+
})
24+
}
525

6-
export default Base =>
7-
class CellEditWrapper extends remoteResolver(Component) {
826
constructor(props) {
927
super(props);
28+
EditingCell = props.cellEdit.editingCellFactory(_);
1029
this.startEditing = this.startEditing.bind(this);
1130
this.escapeEditing = this.escapeEditing.bind(this);
1231
this.completeEditing = this.completeEditing.bind(this);
@@ -21,10 +40,10 @@ export default Base =>
2140

2241
componentWillReceiveProps(nextProps) {
2342
if (nextProps.cellEdit && this.isRemoteCellEdit()) {
24-
if (nextProps.cellEdit.errorMessage) {
43+
if (nextProps.cellEdit.options.errorMessage) {
2544
this.setState(() => ({
2645
isDataChanged: false,
27-
message: nextProps.cellEdit.errorMessage
46+
message: nextProps.cellEdit.options.errorMessage
2847
}));
2948
} else {
3049
this.setState(() => ({
@@ -41,7 +60,7 @@ export default Base =>
4160

4261
handleCellUpdate(row, column, newValue) {
4362
const { keyField, cellEdit, store } = this.props;
44-
const { beforeSaveCell, afterSaveCell } = cellEdit;
63+
const { beforeSaveCell, afterSaveCell } = cellEdit.options;
4564
const oldValue = _.get(row, column.dataField);
4665
const rowId = _.get(row, keyField);
4766
if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column);
@@ -84,17 +103,33 @@ export default Base =>
84103
}
85104

86105
render() {
87-
const { isDataChanged, ...rest } = this.state;
106+
const { isDataChanged, ...stateRest } = this.state;
107+
const {
108+
cellEdit: {
109+
options: { nonEditableRows, errorMessage, ...optionsRest },
110+
editingCellFactory,
111+
...cellEditRest
112+
}
113+
} = this.props;
114+
const newCellEdit = {
115+
...optionsRest,
116+
...cellEditRest,
117+
...stateRest,
118+
EditingCell,
119+
nonEditableRows: _.isDefined(nonEditableRows) ? nonEditableRows() : [],
120+
onStart: this.startEditing,
121+
onEscape: this.escapeEditing,
122+
onUpdate: this.handleCellUpdate
123+
};
124+
88125
return (
89126
<Base
90127
{ ...this.props }
91-
isDataChanged={ isDataChanged }
92128
data={ this.props.store.data }
93-
onCellUpdate={ this.handleCellUpdate }
94-
onStartEditing={ this.startEditing }
95-
onEscapeEditing={ this.escapeEditing }
96-
currEditCell={ { ...rest } }
129+
isDataChanged={ isDataChanged }
130+
cellEdit={ newCellEdit }
97131
/>
98132
);
99133
}
100134
};
135+
};

packages/react-bootstrap-table2/test/cell-edit/editing-cell.test.js renamed to packages/react-bootstrap-table2-editor/test/editing-cell.test.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1+
/* eslint react/prop-types: 0 */
12
import 'jsdom-global/register';
23
import React from 'react';
34
import sinon from 'sinon';
45
import { shallow, mount } from 'enzyme';
56

6-
import { TableRowWrapper } from '../test-helpers/table-wrapper';
7-
import EditingCell from '../../src/cell-edit/editing-cell';
8-
import TextEditor from '../../src/cell-edit/text-editor';
9-
import EditorIndicator from '../../src/cell-edit/editor-indicator';
7+
import _ from 'react-bootstrap-table2/src/utils';
8+
import editingCellFactory from '../src/editing-cell';
9+
import TextEditor from '../src/text-editor';
10+
import EditorIndicator from '../src/editor-indicator';
11+
12+
const EditingCell = editingCellFactory(_);
13+
const TableRowWrapper = props => (
14+
<table>
15+
<tbody>
16+
<tr>{ props.children }</tr>
17+
</tbody>
18+
</table>
19+
);
20+
1021

1122
describe('EditingCell', () => {
1223
let wrapper;

packages/react-bootstrap-table2/test/cell-edit/text-editor.test.js renamed to packages/react-bootstrap-table2-editor/test/text-editor.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'jsdom-global/register';
22
import React from 'react';
33
import { mount } from 'enzyme';
44

5-
import TextEditor from '../../src/cell-edit/text-editor';
5+
import TextEditor from '../src/text-editor';
66

77
describe('TextEditor', () => {
88
let wrapper;

0 commit comments

Comments
 (0)