* implement cell editor

* keyField shouldn't be editable

* add additional check to avoid some bugs

* add jsdom for enzyme mount

* add some helpers class for enzyme mounting

* add testing for cellEdit

* use npm instead

* add test for TableResolver

* table-layout: fixed; to fix the columns width when content of td changed

* add stories for cell edit

* add document for cellEdit
This commit is contained in:
Allen
2017-09-20 11:18:57 -05:00
committed by GitHub
parent 2e10cb132e
commit f6eea2f659
34 changed files with 1503 additions and 108 deletions

View File

@@ -5,7 +5,8 @@ node_js:
- "6"
cache:
yarn: true
directories:
- node_modules
branches:
only:
@@ -13,8 +14,8 @@ branches:
# - master
- develop
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s
- export PATH="$HOME/.yarn/bin:$PATH"
# before_install:
# - curl -o- -L https://yarnpkg.com/install.sh | bash -s
# - export PATH="$HOME/.yarn/bin:$PATH"
install: yarn --frozen-lockfile
# install: yarn --frozen-lockfile

View File

@@ -2,6 +2,18 @@
## Props on BootstrapTable
#### Required
* [keyField (**required**)](#keyField)
* [data (**required**)](#data)
* [columns (**required**)](#columns)
#### Optional
* [striped](#striped)
* [bordered](#bordered)
* [hover](#hover)
* [condensed](#condensed)
* [cellEdit](#cellEdit)
### <a name='keyField'>keyField(**required**) - [String]</a>
`keyField` is a prop to tell `react-bootstrap-table2` which column is unigue key.
@@ -18,4 +30,29 @@ Same as `.table-bordered` class for adding borders on all sides of the table and
### <a name='hover'>hover - [Bool]</a>
Same as `.table-hover` class for adding a hover effect (grey background color) on table rows
### <a name='condensed'>condensed - [Bool]</a>
Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half
Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half
### <a name='cellEdit'>cellEdit - [Bool]</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,
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`)

View File

@@ -25,6 +25,7 @@ Available properties in a column object:
* [headerEvents](#headerEvents)
* [headerAlign](#headerAlign)
* [headerAttrs](#headerAttrs)
* [editable](#editable)
Following is a most simplest and basic usage:
@@ -411,5 +412,9 @@ Additionally, customize the header attributes by a `2-arguments` callback functi
A new `Object` will be the result of element headerAttrs.
#### * Caution
Same as [column.attrs](#attrs), it has lower priority and will be overwrited when other props related to HTML attributes were given.
> Caution:
> Same as [column.attrs](#attrs), it has lower priority and will be
> overwrited when other props related to HTML attributes were given.
## <a name='editable'>column.editable - [Bool]</a>
`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`.

View File

@@ -42,6 +42,8 @@
"eslint-plugin-react": "^7.2.1",
"html-webpack-plugin": "^2.30.1",
"jest": "^20.0.4",
"jsdom": "^11.2.0",
"jsdom-global": "^3.0.2",
"lerna": "^2.0.0",
"node-sass": "^4.5.3",
"react-test-renderer": "^15.6.1",
@@ -71,7 +73,7 @@
],
"testEnvironment": "node",
"testMatch": [
"**/test/**/*.js"
"**/test/**/*.test.js"
]
}
}

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const cellEdit = {
mode: 'click',
blurToSave: true
};
<BootstrapTableful
keyField='id'
data={ products }
columns={ columns }
cellEdit={ cellEdit }
/>
`;
const cellEdit = {
mode: 'click',
blurToSave: true
};
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@@ -0,0 +1,58 @@
/* eslint no-unused-vars: 0 */
/* eslint no-console: 0 */
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const cellEdit = {
mode: 'click',
beforeSaveCell: (oldValue, newValue, row, column) => { console.log('Before Saving Cell!!'); },
afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); }
};
<BootstrapTableful
keyField='id'
data={ products }
columns={ columns }
cellEdit={ cellEdit }
/>
`;
const cellEdit = {
mode: 'click',
beforeSaveCell: (oldValue, newValue, row, column) => { console.log('Before Saving Cell!!'); },
afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); }
};
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const cellEdit = {
mode: 'click'
};
<BootstrapTableful
keyField='id'
data={ products }
columns={ columns }
cellEdit={ cellEdit }
/>
`;
const cellEdit = {
mode: 'click'
};
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name',
editable: false
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
// Product Name column can't be edit anymore
editable: false
}, {
dataField: 'price',
text: 'Product Price'
}];
const cellEdit = {
mode: 'click',
blurToSave: true
};
<BootstrapTableful
keyField='id'
data={ products }
columns={ columns }
cellEdit={ cellEdit }
/>
`;
const cellEdit = {
mode: 'click',
blurToSave: true
};
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const cellEdit = {
mode: 'dbclick'
};
<BootstrapTableful
keyField='id'
data={ products }
columns={ columns }
cellEdit={ cellEdit }
/>
`;
const cellEdit = {
mode: 'dbclick'
};
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'price',
text: 'Product Price'
}];
const cellEdit = {
mode: 'click',
blurToSave: true,
// Product ID: 0, 3 will be non-editable
nonEditableRows: () => [0, 3]
};
<BootstrapTableful
keyField='id'
data={ products }
columns={ columns }
cellEdit={ cellEdit }
/>
`;
const cellEdit = {
mode: 'click',
blurToSave: true,
nonEditableRows: () => [0, 3]
};
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } cellEdit={ cellEdit } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@@ -35,6 +35,14 @@ import HeaderColumnAttrsTable from 'examples/header-columns/column-attrs-table';
import EnableSortTable from 'examples/sort/enable-sort-table';
import CustomSortTable from 'examples/sort/custom-sort-table';
// cell editing
import ClickToEditTable from 'examples/cell-edit/click-to-edit-table';
import DoubleClickToEditTable from 'examples/cell-edit/dbclick-to-edit-table';
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';
// css style
import 'bootstrap/dist/css/bootstrap.min.css';
import 'stories/stylesheet/tomorrow.min.css';
@@ -77,3 +85,11 @@ storiesOf('Work on Header Columns', module)
storiesOf('Sort Table', module)
.add('Enable Sort', () => <EnableSortTable />)
.add('Custom Sort Fuction', () => <CustomSortTable />);
storiesOf('Cell Editing', module)
.add('Click to Edit', () => <ClickToEditTable />)
.add('DoubleClick to Edit', () => <DoubleClickToEditTable />)
.add('Blur to Save Cell', () => <BlurToSaveTable />)
.add('Row Level Editable', () => <RowLevelEditableTable />)
.add('Column Level Editable', () => <ColumnLevelEditableTable />)
.add('Rich Hook Functions', () => <CellEditHooks />);

View File

@@ -13,7 +13,8 @@ const Body = (props) => {
keyField,
isEmpty,
noDataIndication,
visibleColumnSize
visibleColumnSize,
cellEdit
} = props;
let content;
@@ -22,14 +23,21 @@ const Body = (props) => {
const indication = _.isFunction(noDataIndication) ? noDataIndication() : noDataIndication;
content = <RowSection content={ indication } colSpan={ visibleColumnSize } />;
} else {
content = data.map((row, index) => (
<Row
key={ _.get(row, keyField) }
row={ row }
rowIndex={ index }
columns={ columns }
/>
));
content = data.map((row, index) => {
const key = _.get(row, keyField);
const editable = !(cellEdit && cellEdit.nonEditableRows.indexOf(key) > -1);
return (
<Row
key={ key }
row={ row }
keyField={ keyField }
rowIndex={ index }
columns={ columns }
cellEdit={ cellEdit }
editable={ editable }
/>
);
});
}
return (

View File

@@ -7,6 +7,8 @@ import Header from './header';
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) {
@@ -16,8 +18,15 @@ class BootstrapTable extends PropsBaseResolver(Component) {
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.state = {
data: this.store.get()
data: this.store.get(),
currEditCell: {
ridx: null,
cidx: null
}
};
}
@@ -39,6 +48,12 @@ class BootstrapTable extends PropsBaseResolver(Component) {
'table-condensed': condensed
});
const cellEditInfo = this.resolveCellEditProps({
onStart: this.startEditing,
onEscape: this.escapeEditing,
onComplete: this.completeEditing
});
return (
<div className="react-bootstrap-table-container">
<table className={ tableClass }>
@@ -55,6 +70,7 @@ class BootstrapTable extends PropsBaseResolver(Component) {
isEmpty={ this.isEmpty() }
visibleColumnSize={ this.visibleColumnSize() }
noDataIndication={ noDataIndication }
cellEdit={ cellEditInfo }
/>
</table>
</div>
@@ -70,6 +86,39 @@ class BootstrapTable extends PropsBaseResolver(Component) {
};
});
}
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 }
};
});
}
}
BootstrapTable.propTypes = {
@@ -81,7 +130,15 @@ BootstrapTable.propTypes = {
striped: PropTypes.bool,
bordered: PropTypes.bool,
hover: PropTypes.bool,
condensed: PropTypes.bool
condensed: PropTypes.bool,
cellEdit: PropTypes.shape({
mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired,
onEditing: PropTypes.func.isRequired,
blurToSave: PropTypes.bool,
beforeSaveCell: PropTypes.func,
afterSaveCell: PropTypes.func,
nonEditableRows: PropTypes.func
})
};
BootstrapTable.defaultProps = {

View File

@@ -1,63 +1,103 @@
import React from 'react';
/* eslint react/prop-types: 0 */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Const from './const';
import _ from './utils';
const Cell = ({ row, rowIndex, column, columnIndex }) => {
const {
dataField,
hidden,
formatter,
formatExtraData,
style,
classes,
title,
events,
align,
attrs
} = column;
let cellTitle;
let cellStyle = {};
let content = _.get(row, dataField);
const cellAttrs = {
..._.isFunction(attrs) ? attrs(content, row, rowIndex, columnIndex) : attrs,
...events
};
const cellClasses = _.isFunction(classes)
? classes(content, row, rowIndex, columnIndex)
: classes;
if (style) {
cellStyle = _.isFunction(style) ? style(content, row, rowIndex, columnIndex) : style;
class Cell extends Component {
constructor(props) {
super(props);
this.handleEditingCell = this.handleEditingCell.bind(this);
}
if (title) {
cellTitle = _.isFunction(title) ? title(content, row, rowIndex, columnIndex) : content;
cellAttrs.title = cellTitle;
handleEditingCell(e) {
const { editMode, column, onStart, rowIndex, columnIndex } = this.props;
const { events } = column;
if (events) {
if (editMode === Const.CLICK_TO_CELL_EDIT) {
const customClick = events.onClick;
if (_.isFunction(customClick)) customClick(e);
} else {
const customDbClick = events.onDoubleClick;
if (_.isFunction(customDbClick)) customDbClick(e);
}
}
onStart(rowIndex, columnIndex);
}
if (formatter) {
content = column.formatter(content, row, rowIndex, formatExtraData);
render() {
const {
row,
rowIndex,
column,
columnIndex,
editMode,
editable
} = this.props;
const {
dataField,
hidden,
formatter,
formatExtraData,
style,
classes,
title,
events,
align,
attrs
} = column;
let cellTitle;
let cellStyle = {};
let content = _.get(row, dataField);
const cellAttrs = {
..._.isFunction(attrs) ? attrs(content, row, rowIndex, columnIndex) : attrs,
...events
};
const cellClasses = _.isFunction(classes)
? classes(content, row, rowIndex, columnIndex)
: classes;
if (style) {
cellStyle = _.isFunction(style) ? style(content, row, rowIndex, columnIndex) : style;
}
if (title) {
cellTitle = _.isFunction(title) ? title(content, row, rowIndex, columnIndex) : content;
cellAttrs.title = cellTitle;
}
if (formatter) {
content = column.formatter(content, row, rowIndex, formatExtraData);
}
if (align) {
cellStyle.textAlign =
_.isFunction(align) ? align(content, row, rowIndex, columnIndex) : align;
}
if (hidden) {
cellStyle.display = 'none';
}
if (cellClasses) cellAttrs.className = cellClasses;
if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle;
if (editable && editMode !== Const.UNABLE_TO_CELL_EDIT) {
if (editMode === Const.CLICK_TO_CELL_EDIT) { // click to edit
cellAttrs.onClick = this.handleEditingCell;
} else { // dbclick to edit
cellAttrs.onDoubleClick = this.handleEditingCell;
}
}
return (
<td { ...cellAttrs }>{ content }</td>
);
}
if (align) {
cellStyle.textAlign = _.isFunction(align) ? align(content, row, rowIndex, columnIndex) : align;
}
if (hidden) {
cellStyle.display = 'none';
}
if (cellClasses) cellAttrs.className = cellClasses;
if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle;
return (
<td { ...cellAttrs }>{ content }</td>
);
};
}
Cell.propTypes = {
row: PropTypes.object.isRequired,

View File

@@ -1,4 +1,7 @@
export default {
SORT_ASC: 'asc',
SORT_DESC: 'desc'
SORT_DESC: 'desc',
UNABLE_TO_CELL_EDIT: 'none',
CLICK_TO_CELL_EDIT: 'click',
DBCLICK_TO_CELL_EDIT: 'dbclick'
};

View File

@@ -0,0 +1,66 @@
/* eslint react/prop-types: 0 */
/* eslint no-return-assign: 0 */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _ from './utils';
import TextEditor from './text-editor';
class EditingCell extends Component {
constructor(props) {
super(props);
this.handleBlur = this.handleBlur.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
handleBlur() {
const { onEscape, onComplete, 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);
} else {
onEscape();
}
}
handleKeyDown(e) {
const { onEscape, onComplete, row, column } = this.props;
if (e.keyCode === 27) { // ESC
onEscape();
} else if (e.keyCode === 13) { // ENTER
const value = e.currentTarget.value;
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
onComplete(row, column, value);
}
}
render() {
const { row, column } = this.props;
const { dataField } = column;
const value = _.get(row, dataField);
const editorAttrs = {
onKeyDown: this.handleKeyDown,
onBlur: this.handleBlur
};
return (
<td>
<TextEditor ref={ node => this.editor = node } defaultValue={ value } { ...editorAttrs } />
</td>
);
}
}
EditingCell.propTypes = {
row: PropTypes.object.isRequired,
column: PropTypes.object.isRequired,
onComplete: PropTypes.func.isRequired,
onEscape: PropTypes.func.isRequired
};
export default EditingCell;

View File

@@ -1,4 +1,6 @@
import ColumnResolver from './column-resolver';
import Const from '../const';
import _ from '../utils';
export default ExtendBase =>
class TableResolver extends ColumnResolver(ExtendBase) {
@@ -15,4 +17,27 @@ export default ExtendBase =>
isEmpty() {
return this.props.data.length === 0;
}
resolveCellEditProps(options) {
const { cellEdit } = this.props;
const { currEditCell } = this.state;
const nonEditableRows =
(cellEdit && _.isFunction(cellEdit.nonEditableRows)) ? cellEdit.nonEditableRows() : [];
const cellEditInfo = {
...currEditCell,
nonEditableRows
};
if (_.isDefined(cellEdit)) {
return {
...cellEdit,
...cellEditInfo,
...options
};
}
return {
mode: Const.UNABLE_TO_CELL_EDIT,
...cellEditInfo
};
}
};

View File

@@ -1,25 +1,64 @@
/* eslint react/prop-types: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import _ from './utils';
import Cell from './cell';
import EditingCell from './editing-cell';
const Row = ({ row, rowIndex, columns }) => (
<tr>
{
columns.map((column, index) =>
(
<Cell
key={ _.get(row, column.dataField) }
row={ row }
rowIndex={ rowIndex }
columnIndex={ index }
column={ column }
/>
))
}
</tr>
);
const Row = (props) => {
const {
row,
columns,
keyField,
rowIndex,
cellEdit,
editable: editableRow
} = props;
const {
ridx: editingRowIdx,
cidx: editingColIdx,
mode,
onStart,
onEscape,
onComplete,
blurToSave
} = cellEdit;
return (
<tr>
{
columns.map((column, index) => {
let editable = _.isDefined(column.editable) ? column.editable : true;
if (column.dataField === keyField || !editableRow) editable = false;
if (rowIndex === editingRowIdx && index === editingColIdx) {
return (
<EditingCell
key={ _.get(row, column.dataField) }
row={ row }
column={ column }
blurToSave={ blurToSave }
onComplete={ onComplete }
onEscape={ onEscape }
/>
);
}
return (
<Cell
key={ _.get(row, column.dataField) }
row={ row }
rowIndex={ rowIndex }
columnIndex={ index }
column={ column }
editMode={ mode }
editable={ editable }
onStart={ onStart }
/>
);
})
}
</tr>
);
};
Row.propTypes = {
row: PropTypes.object.isRequired,
@@ -27,4 +66,8 @@ Row.propTypes = {
columns: PropTypes.array.isRequired
};
Row.defaultProps = {
editable: true
};
export default Row;

View File

@@ -7,11 +7,20 @@ const withStateful = (Base) => {
constructor(props) {
super(props);
this.store = new Store(props);
this.edit = this.edit.bind(this);
}
edit(rowId, dataField, newValue) {
this.store.edit(rowId, dataField, newValue);
}
render() {
const { props } = this;
return <Base { ...props } store={ this.store } />;
const newProps = { ...props };
if (newProps.cellEdit && !newProps.cellEdit.onEditing) {
newProps.cellEdit.onEditing = this.edit;
}
return <Base { ...newProps } store={ this.store } />;
}
}
return StatefulComponent;

View File

@@ -1,9 +1,11 @@
import { sort } from './sort';
import Const from '../const';
import _ from '../utils';
export default class Store {
constructor(props) {
const { data } = props;
const { data, keyField } = props;
this.keyField = keyField;
this.data = data ? data.slice() : [];
this.sortOrder = undefined;
@@ -25,7 +27,16 @@ export default class Store {
this.sortField = dataField;
}
edit(rowId, dataField, newValue) {
const row = this.getRowByRowId(rowId);
if (row) _.set(row, dataField, newValue);
}
get() {
return this.data;
}
getRowByRowId(rowId) {
return this.get().find(row => _.get(row, this.keyField) === rowId);
}
}

View File

@@ -0,0 +1,21 @@
/* eslint no-return-assign: 0 */
import React, { Component } from 'react';
class TextEditor extends Component {
componentDidMount() {
this.text.focus();
}
render() {
return (
<input
ref={ node => this.text = node }
type="text"
className="form-control editor edit-text"
{ ...this.props }
/>
);
}
}
export default TextEditor;

View File

@@ -1,11 +1,16 @@
/* eslint no-empty: 0 */
/* eslint no-param-reassign: 0 */
function get(target, field) {
const pathArray = [field]
function splitNested(str) {
return [str]
.join('.')
.replace(/\[/g, '.')
.replace(/\]/g, '')
.split('.');
}
function get(target, field) {
const pathArray = splitNested(field);
let result;
try {
result = pathArray.reduce((curr, path) => curr[path], target);
@@ -13,6 +18,25 @@ function get(target, field) {
return result;
}
function set(target, field, value, safe = false) {
const pathArray = splitNested(field);
let level = 0;
pathArray.reduce((a, b) => {
level += 1;
if (typeof a[b] === 'undefined') {
if (!safe) throw new Error(`${a}.${b} is undefined`);
a[b] = {};
return a[b];
}
if (level === pathArray.length) {
a[b] = value;
return value;
}
return a[b];
}, target);
}
function isFunction(obj) {
return obj && (typeof obj === 'function');
}
@@ -46,6 +70,7 @@ function isDefined(value) {
export default {
get,
set,
isFunction,
isObject,
isEmptyObject,

View File

@@ -1,5 +1,9 @@
.react-bootstrap-table-container {
table {
table-layout: fixed;
}
th.sortable {
cursor: pointer;
}

View File

@@ -4,6 +4,7 @@ import { shallow } from 'enzyme';
import Body from '../src/body';
import Row from '../src/row';
import Const from '../src/const';
import RowSection from '../src/row-section';
describe('Body', () => {
@@ -111,4 +112,35 @@ describe('Body', () => {
});
});
});
describe('when cellEdit.nonEditableRows props is defined', () => {
const nonEditableRows = [data[1].id];
const keyField = 'id';
const cellEdit = {
mode: Const.CLICK_TO_CELL_EDIT,
nonEditableRows
};
beforeEach(() => {
wrapper = shallow(
<Body
data={ data }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
/>
);
});
it('should render Row component with correct editable prop', () => {
expect(wrapper.length).toBe(1);
const rows = wrapper.find(Row);
for (let i = 0; i < rows.length; i += 1) {
if (nonEditableRows.indexOf(rows.get(i).props.row[keyField]) > -1) {
expect(rows.get(i).props.editable).toBeFalsy();
} else {
expect(rows.get(i).props.editable).toBeTruthy();
}
}
});
});
});

View File

@@ -1,9 +1,11 @@
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import Header from '../src/header';
import Body from '../src/body';
import BootstrapTable from '../src/bootstrap-table';
import Const from '../src/const';
describe('BootstrapTable', () => {
let wrapper;
@@ -36,6 +38,12 @@ describe('BootstrapTable', () => {
expect(wrapper.find('.react-bootstrap-table-container').length).toBe(1);
});
it('should have correct default state', () => {
expect(wrapper.state().currEditCell).toBeDefined();
expect(wrapper.state().currEditCell.ridx).toBeNull();
expect(wrapper.state().currEditCell.cidx).toBeNull();
});
it('should have table-bordered class as default', () => {
expect(wrapper.find('table.table-bordered').length).toBe(1);
});
@@ -80,4 +88,36 @@ describe('BootstrapTable', () => {
expect(wrapper.find('table.table-condensed').length).toBe(0);
});
});
describe('when cellEdit props is defined', () => {
const nonEditableRows = [data[1].id];
const cellEdit = {
mode: Const.CLICK_TO_CELL_EDIT,
onEditing: sinon.stub(),
nonEditableRows: () => nonEditableRows
};
beforeEach(() => {
wrapper = shallow(
<BootstrapTable
keyField="id"
columns={ columns }
data={ data }
bordered={ false }
cellEdit={ cellEdit }
/>
);
});
it('should resolve correct cellEdit object to Body component', () => {
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.onStart).toBeDefined();
expect(body.props().cellEdit.onEscape).toBeDefined();
expect(body.props().cellEdit.onComplete).toBeDefined();
});
});
});

View File

@@ -2,6 +2,7 @@ import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import Const from '../src/const';
import Cell from '../src/cell';
describe('Cell', () => {
@@ -447,4 +448,98 @@ describe('Cell', () => {
});
});
});
describe('when editable prop is true', () => {
let onStartCallBack;
const rowIndex = 1;
const columnIndex = 1;
const column = {
dataField: 'id',
text: 'ID'
};
beforeEach(() => {
onStartCallBack = sinon.stub().withArgs(rowIndex, columnIndex);
});
describe(`and editMode is ${Const.CLICK_TO_CELL_EDIT}`, () => {
beforeEach(() => {
wrapper = shallow(
<Cell
row={ row }
rowIndex={ rowIndex }
column={ column }
columnIndex={ columnIndex }
editable
editMode={ Const.CLICK_TO_CELL_EDIT }
onStart={ onStartCallBack }
/>
);
});
it('should render onClick attribute', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('td').prop('onClick')).toBeDefined();
});
it('should call onStart correctly when clicking cell', () => {
wrapper.find('td').simulate('click');
expect(onStartCallBack.callCount).toBe(1);
expect(onStartCallBack.calledWith(rowIndex, columnIndex)).toBe(true);
});
describe('if when column.events.onClick prop is defined', () => {
beforeEach(() => {
column.events = {
onClick: sinon.stub()
};
});
it('should calling custom onClick callback also', () => {
wrapper.find('td').simulate('click');
expect(onStartCallBack.callCount).toBe(1);
expect(column.events.onClick.callCount).toBe(1);
});
});
});
describe(`and editMode is ${Const.DBCLICK_TO_CELL_EDIT}`, () => {
beforeEach(() => {
wrapper = shallow(
<Cell
row={ row }
rowIndex={ 1 }
column={ column }
columnIndex={ 1 }
editable
editMode={ Const.DBCLICK_TO_CELL_EDIT }
onStart={ onStartCallBack }
/>
);
});
it('should render onDoubleClick attribute', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('td').prop('onDoubleClick')).toBeDefined();
});
it('should call onStart correctly when double clicking cell', () => {
wrapper.find('td').simulate('doubleclick');
expect(onStartCallBack.callCount).toBe(1);
expect(onStartCallBack.calledWith(rowIndex, columnIndex)).toBe(true);
});
describe('if when column.events.onDoubleClick prop is defined', () => {
beforeEach(() => {
column.events = {
onDoubleClick: sinon.stub()
};
});
it('should calling custom onDoubleClick callback also', () => {
wrapper.find('td').simulate('doubleclick');
expect(onStartCallBack.callCount).toBe(1);
expect(column.events.onDoubleClick.callCount).toBe(1);
});
});
});
});
});

View File

@@ -0,0 +1,93 @@
import 'jsdom-global/register';
import React from 'react';
import sinon from 'sinon';
import { shallow, mount } from 'enzyme';
import { TableRowWrapper } from './test-helpers/table-wrapper';
import EditingCell from '../src/editing-cell';
import TextEditor from '../src/text-editor';
describe('EditingCell', () => {
let wrapper;
let onEscape;
let onComplete;
const row = {
id: 1,
name: 'A'
};
const column = {
dataField: 'id',
text: 'ID'
};
beforeEach(() => {
onComplete = sinon.stub();
onEscape = sinon.stub();
wrapper = shallow(
<EditingCell
row={ row }
column={ column }
onEscape={ onEscape }
onComplete={ onComplete }
/>
);
});
it('should render default editor successfully', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('td').length).toBe(1);
expect(wrapper.find(TextEditor).length).toBe(1);
});
it('should render TextEditor with correct props', () => {
const textEditor = wrapper.find(TextEditor);
expect(textEditor.props().defaultValue).toEqual(row[column.dataField]);
expect(textEditor.props().onKeyDown).toBeDefined();
expect(textEditor.props().onBlur).toBeDefined();
});
it('when press ENTER on TextEditor should call onComplete 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);
});
it('when press ESC on TextEditor should call onEscape correctly', () => {
const textEditor = wrapper.find(TextEditor);
textEditor.simulate('keyDown', { keyCode: 27 });
expect(onEscape.callCount).toBe(1);
});
it('when blur from TextEditor should call onEscape correctly', () => {
const textEditor = wrapper.find(TextEditor);
textEditor.simulate('blur');
expect(onEscape.callCount).toBe(1);
});
describe('if blurToSave prop is true', () => {
beforeEach(() => {
wrapper = mount(
<TableRowWrapper>
<EditingCell
row={ row }
column={ column }
onEscape={ onEscape }
onComplete={ onComplete }
blurToSave
/>
</TableRowWrapper>
);
});
it('when blur from TextEditor should call onComplete correctly', () => {
const textEditor = wrapper.find(TextEditor);
textEditor.simulate('blur');
expect(onComplete.callCount).toBe(1);
expect(onComplete.calledWith(row, column, `${row[column.dataField]}`)).toBe(true);
});
});
});

View File

@@ -0,0 +1,146 @@
import React, { Component } from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { extendTo } from '../test-helpers/mock-component';
import baseResolver from '../../src/props-resolver/index';
import Const from '../../src/const';
describe('TableResolver', () => {
const keyField = 'id';
const columns = [{
dataField: keyField,
text: 'ID'
}, {
dataField: 'name',
text: 'Name'
}];
const data = [{
id: 1,
name: 'A'
}, {
id: 2,
name: 'B'
}];
const ExtendBase = baseResolver(Component);
const BootstrapTableMock = extendTo(ExtendBase);
let wrapper;
describe('validateProps', () => {
describe('if keyField is defined and columns is all visible', () => {
beforeEach(() => {
const mockElement = React.createElement(BootstrapTableMock, {
data, columns, keyField
}, null);
wrapper = shallow(mockElement);
});
it('should not throw any errors', () => {
expect(() => wrapper.instance().validateProps()).not.toThrow();
});
});
describe('if keyField is not defined on props', () => {
beforeEach(() => {
const mockElement = React.createElement(BootstrapTableMock, {
data, columns
}, null);
wrapper = shallow(mockElement);
});
it('should throw error', () => {
expect(() =>
wrapper.instance().validateProps()
).toThrow(new Error('Please specify a field as key via keyField'));
});
});
describe('if columns is all unvisible', () => {
beforeEach(() => {
const mockElement = React.createElement(BootstrapTableMock, {
data, keyField, columns: []
}, null);
wrapper = shallow(mockElement);
});
it('should throw error', () => {
expect(() =>
wrapper.instance().validateProps()
).toThrow(new Error('No any visible columns detect'));
});
});
});
describe('resolveCellEditProps', () => {
describe('if cellEdit prop not defined', () => {
beforeEach(() => {
const mockElement = React.createElement(BootstrapTableMock, {
data, keyField, columns
}, null);
wrapper = shallow(mockElement);
});
it('should resolve a default cellEdit instance', () => {
const cellEdit = wrapper.instance().resolveCellEditProps();
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);
});
});
});
describe('if cellEdit prop defined', () => {
const expectNonEditableRows = [1, 2];
const cellEdit = {
mode: Const.DBCLICK_TO_CELL_EDIT,
onEditing: sinon.stub(),
blurToSave: true,
beforeSaveCell: sinon.stub(),
afterSaveCell: sinon.stub(),
nonEditableRows: sinon.stub().returns(expectNonEditableRows)
};
beforeEach(() => {
const mockElement = React.createElement(BootstrapTableMock, {
data, keyField, columns, cellEdit
}, null);
wrapper = shallow(mockElement);
});
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.blurToSave).toEqual(cellEdit.blurToSave);
expect(cellEditInfo.beforeSaveCell).toEqual(cellEdit.beforeSaveCell);
expect(cellEditInfo.afterSaveCell).toEqual(cellEdit.afterSaveCell);
expect(cellEditInfo.nonEditableRows).toEqual(expectNonEditableRows);
});
it('should attach options to cellEdit props', () => {
const something = {
test: 1,
cb: sinon.stub()
};
const cellEditInfo = wrapper.instance().resolveCellEditProps(something);
expect(cellEditInfo).toBeDefined();
expect(cellEditInfo.test).toEqual(something.test);
expect(cellEditInfo.cb).toEqual(something.cb);
});
});
});

View File

@@ -1,27 +1,36 @@
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import Cell from '../src/cell';
import Row from '../src/row';
import Const from '../src/const';
import EditingCell from '../src/editing-cell';
const defaultColumns = [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'name',
text: 'Name'
}, {
dataField: 'price',
text: 'Price'
}];
describe('Row', () => {
let wrapper;
const columns = [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'name',
text: 'Name'
}];
const row = {
id: 1,
name: 'A'
name: 'A',
price: 1000
};
describe('simplest row', () => {
beforeEach(() => {
wrapper = shallow(<Row rowIndex={ 1 } columns={ columns } row={ row } />);
wrapper = shallow(
<Row rowIndex={ 1 } columns={ defaultColumns } row={ row } cellEdit={ {} } />);
});
it('should render successfully', () => {
@@ -30,4 +39,158 @@ describe('Row', () => {
expect(wrapper.find(Cell).length).toBe(Object.keys(row).length);
});
});
describe('when cellEdit prop is defined', () => {
let columns;
let cellEdit;
const rowIndex = 1;
const keyField = 'id';
beforeEach(() => {
columns = defaultColumns;
cellEdit = {
mode: Const.CLICK_TO_CELL_EDIT
};
wrapper = shallow(
<Row
row={ row }
rowIndex={ rowIndex }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
/>
);
});
afterEach(() => {
columns = undefined;
cellEdit = undefined;
});
it('Cell component should receive correct editable props', () => {
expect(wrapper.length).toBe(1);
for (let i = 0; i < columns.length; i += 1) {
const column = columns[i];
if (column.dataField === keyField) {
expect(wrapper.find(Cell).get(i).props.editable).toBeFalsy();
} else {
expect(wrapper.find(Cell).get(i).props.editable).toBeTruthy();
}
}
});
it('Cell component should receive correct editMode props', () => {
expect(wrapper.length).toBe(1);
for (let i = 0; i < columns.length; i += 1) {
expect(wrapper.find(Cell).get(i).props.editMode).toEqual(cellEdit.mode);
}
});
describe('when have column.editable defined false', () => {
const nonEditableColIndex = 1;
beforeEach(() => {
columns[nonEditableColIndex].editable = false;
wrapper = shallow(
<Row
row={ row }
rowIndex={ rowIndex }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
/>
);
});
it('Cell component should receive correct editMode props', () => {
expect(wrapper.length).toBe(1);
for (let i = 0; i < columns.length; i += 1) {
const column = columns[i];
if (i === nonEditableColIndex || column.dataField === keyField) {
expect(wrapper.find(Cell).get(i).props.editable).toBeFalsy();
} else {
expect(wrapper.find(Cell).get(i).props.editable).toBeTruthy();
}
}
});
});
// Means user defined cellEdit.nonEditableRows
// and some rows will be treated as noneditable by this rules
describe('when editable prop is false', () => {
beforeEach(() => {
wrapper = shallow(
<Row
row={ row }
rowIndex={ rowIndex }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
editable={ false }
/>
);
});
it('All the Cell components should be noneditable', () => {
expect(wrapper.length).toBe(1);
for (let i = 0; i < columns.length; i += 1) {
expect(wrapper.find(Cell).get(i).props.editable).toBeFalsy();
}
});
});
// Means a cell now is undering editing
describe('when cellEdit.ridx and cellEdit.cidx is defined', () => {
describe('and cellEdit.ridx is match to current row index', () => {
const editingColIndex = 1;
beforeEach(() => {
cellEdit.ridx = rowIndex;
cellEdit.cidx = editingColIndex;
cellEdit.onComplete = sinon.stub();
cellEdit.onEscape = sinon.stub();
wrapper = shallow(
<Row
row={ row }
rowIndex={ 1 }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
editable={ false }
/>
);
});
it('should render EditingCell correctly', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find(EditingCell).length).toBe(1);
expect(wrapper.find('tr').children().at(editingColIndex).type()).toEqual(EditingCell);
});
});
describe('and cellEdit.ridx is not match to current row index', () => {
const editingColIndex = 1;
beforeEach(() => {
cellEdit.ridx = 3;
cellEdit.cidx = editingColIndex;
cellEdit.onComplete = sinon.stub();
cellEdit.onEscape = sinon.stub();
wrapper = shallow(
<Row
row={ row }
rowIndex={ 1 }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
editable={ false }
/>
);
});
it('should not render any EditingCell component', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find(EditingCell).length).toBe(0);
expect(wrapper.find(Cell).length).toBe(columns.length);
});
});
});
});
});

View File

@@ -3,15 +3,16 @@ import Const from '../../src/const';
describe('Store Base', () => {
let store;
const data = [
{ id: 3, name: 'name2' },
{ id: 2, name: 'ABC' },
{ id: 4, name: '123tester' },
{ id: 1, name: '!@#' }
];
let data;
beforeEach(() => {
store = new Base({ data });
data = [
{ id: 3, name: 'name2' },
{ id: 2, name: 'ABC' },
{ id: 4, name: '123tester' },
{ id: 1, name: '!@#' }
];
store = new Base({ data, keyField: 'id' });
});
describe('initialize', () => {
@@ -72,4 +73,40 @@ describe('Store Base', () => {
});
});
});
describe('getRowByRowId', () => {
it('should return data if specified id existing', () => {
const result = store.getRowByRowId(3);
expect(result).toBeDefined();
});
it('should return undefined if specified id existing', () => {
const result = store.getRowByRowId(88);
expect(result).toBeUndefined();
});
});
describe('edit', () => {
it('should update a specified field correctly', () => {
const newValue = 'newValue';
const dataField = 'name';
const rowId = 2;
store.edit(rowId, dataField, newValue);
const row = store.data.find(d => d[store.keyField] === rowId);
expect(row[dataField]).toEqual(newValue);
});
it('should not throw any error even if rowId is not existing', () => {
expect(() => {
store.edit('123', 'name', 'value');
}).not.toThrow();
});
it('should throwing error if dataField is not existing', () => {
expect(() => {
store.edit(2, 'non_exist_field', 'value');
}).toThrow();
});
});
});

View File

@@ -0,0 +1,15 @@
export const extendTo = Base =>
class MockComponent extends Base {
constructor(props) {
super(props);
this.state = {
data: this.props.data,
currEditCell: {
ridx: null,
cidx: null
}
};
}
render() { return null; }
};

View File

@@ -0,0 +1,11 @@
/* eslint react/prop-types: 0 */
import React from 'react';
export const TableRowWrapper = props => (
<table>
<tbody>
<tr>{ props.children }</tr>
</tbody>
</table>
);

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import TextEditor from '../src/text-editor';
describe('TextEditor', () => {
let wrapper;
const value = 'test';
beforeEach(() => {
wrapper = shallow(
<TextEditor
defaultValue={ value }
/>
);
});
it('should render TextEditor correctly', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('input').length).toBe(1);
expect(wrapper.find('input').prop('type')).toEqual('text');
expect(wrapper.find('.form-control.editor.edit-text').length).toBe(1);
});
});

View File

@@ -22,6 +22,42 @@ describe('Utils', () => {
});
});
describe('set', () => {
const newValue = 'test';
const data = {
name: 'A',
address: {
road: 'BCD',
postal: '1234-12345',
city: {
name: 'B'
}
}
};
it('should set data successfully', () => {
_.set(data, 'name', newValue);
_.set(data, 'address.road', newValue);
_.set(data, 'address.city.name', newValue);
expect(data.name).toEqual(newValue);
expect(data.address.road).toEqual(newValue);
expect(data.address.city.name).toEqual(newValue);
});
it('should throw error if target not existing', () => {
expect(() => {
_.set(data, 'address.not.existing', newValue);
}).toThrow();
});
it('should not throw error if target not existing but with safe=true', () => {
expect(() => {
_.set(data, 'address.not.existing', newValue, true);
}).not.toThrow();
expect(data.address.not.existing).toEqual({});
});
});
describe('isObject', () => {
describe('when given Object', () => {
it('should return true', () => {