diff --git a/docs/columns.md b/docs/columns.md index 89c62cf..330b7ad 100644 --- a/docs/columns.md +++ b/docs/columns.md @@ -95,6 +95,10 @@ dataField: 'address.city' * `rowIndex` * [`formatExtraData`](#formatExtraData) +> Attention: +> Don't use any state data or any external data in formatter function, please pass them via [`formatExtraData`](#formatExtraData). +> In addition, please make formatter function as pure function as possible as you can. + ## column.headerFormatter - [Function] `headerFormatter` allow you to customize the header column and only accept a callback function which take three arguments and a JSX/String are expected for return. diff --git a/package.json b/package.json index 5cb8003..ae5416b 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,8 @@ "classnames": "2.2.5", "prop-types": "15.5.10", "react": "16.3.2", - "react-dom": "16.3.2" + "react-dom": "16.3.2", + "underscore": "1.9.1" }, "jest": { "collectCoverageFrom": [ diff --git a/packages/react-bootstrap-table2-example/examples/basic/large-table.js b/packages/react-bootstrap-table2-example/examples/basic/large-table.js new file mode 100644 index 0000000..671aaa5 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/basic/large-table.js @@ -0,0 +1,32 @@ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import cellEditFactory from 'react-bootstrap-table2-editor'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(5000); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +export default () => ( +
+ +
+); diff --git a/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js b/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js index 7e7bfcb..5fa6fcf 100644 --- a/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js +++ b/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js @@ -41,7 +41,7 @@ const columns = [{ export default () => (
-

Try to hover on Product Name header column

+

Try to hover on Product ID Cell

{ sourceCode }
diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index eee2c87..24db59c 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -11,6 +11,7 @@ import StripHoverCondensedTable from 'examples/basic/striped-hover-condensed-tab import NoDataTable from 'examples/basic/no-data-table'; import CustomizedIdClassesTable from 'examples/basic/customized-id-classes'; import CaptionTable from 'examples/basic/caption-table'; +import LargeTable from 'examples/basic/large-table'; // work on columns import NestedDataTable from 'examples/columns/nested-data-table'; @@ -174,7 +175,8 @@ storiesOf('Basic Table', module) .add('borderless table', () => ) .add('Indication For Empty Table', () => ) .add('Customized id and class table', () => ) - .add('Table with caption', () => ); + .add('Table with caption', () => ) + .add('Large Table', () => ); storiesOf('Work on Columns', module) .add('Display Nested Data', () => ) diff --git a/packages/react-bootstrap-table2-paginator/src/page-resolver.js b/packages/react-bootstrap-table2-paginator/src/page-resolver.js index 8cfeb10..5146faf 100644 --- a/packages/react-bootstrap-table2-paginator/src/page-resolver.js +++ b/packages/react-bootstrap-table2-paginator/src/page-resolver.js @@ -40,8 +40,8 @@ export default ExtendBase => let from = ((currPage - pageStartIndex) * currSizePerPage); from = dataSize === 0 ? 0 : from + 1; - let to = Math.min((currSizePerPage * (currPage + offset) - 1), dataSize); - if (to >= dataSize) to -= 1; + let to = Math.min(currSizePerPage * (currPage + offset), dataSize); + if (to > dataSize) to = dataSize; return [from, to]; } diff --git a/packages/react-bootstrap-table2-paginator/test/page-resolver.test.js b/packages/react-bootstrap-table2-paginator/test/page-resolver.test.js index 1eaf996..7dbc198 100644 --- a/packages/react-bootstrap-table2-paginator/test/page-resolver.test.js +++ b/packages/react-bootstrap-table2-paginator/test/page-resolver.test.js @@ -119,7 +119,34 @@ describe('PageResolver', () => { it('should return correct array with from and to value', () => { const instance = wrapper.instance(); - expect(instance.calculateFromTo()).toEqual([1, props.currSizePerPage - 1]); + expect(instance.calculateFromTo()).toEqual([1, props.currSizePerPage]); + }); + + describe('if data is empty', () => { + beforeEach(() => { + props.dataSize = 87; + props.currPage = 9; + const mockElement = React.createElement(MockComponent, props, null); + wrapper = shallow(mockElement); + }); + + it('should return correct array with from and to value', () => { + const instance = wrapper.instance(); + expect(instance.calculateFromTo()).toEqual([81, props.dataSize]); + }); + }); + + describe('if current page is last page', () => { + beforeEach(() => { + props.dataSize = 0; + const mockElement = React.createElement(MockComponent, props, null); + wrapper = shallow(mockElement); + }); + + it('should return correct array with from and to value', () => { + const instance = wrapper.instance(); + expect(instance.calculateFromTo()).toEqual([0, 0]); + }); }); }); diff --git a/packages/react-bootstrap-table2-toolkit/README.md b/packages/react-bootstrap-table2-toolkit/README.md index 5d502e1..f26c366 100644 --- a/packages/react-bootstrap-table2-toolkit/README.md +++ b/packages/react-bootstrap-table2-toolkit/README.md @@ -120,4 +120,7 @@ Custom the csv file separator. Default is `false`. Give true to avoid to attach the csv header. #### noAutoBOM - [bool] -Default is `true`. \ No newline at end of file +Default is `true`. + +#### exportAll - [bool] +Default is `true`. `false` will only export current data which display on table. \ No newline at end of file diff --git a/packages/react-bootstrap-table2-toolkit/context.js b/packages/react-bootstrap-table2-toolkit/context.js index 4ae5e19..d12a7f0 100644 --- a/packages/react-bootstrap-table2-toolkit/context.js +++ b/packages/react-bootstrap-table2-toolkit/context.js @@ -48,7 +48,9 @@ class ToolkitProvider extends statelessDrcorator(React.Component) { } onSearch(searchText) { - this.setState({ searchText }); + if (searchText !== this.state.searchText) { + this.setState({ searchText }); + } } /** @@ -69,7 +71,8 @@ class ToolkitProvider extends statelessDrcorator(React.Component) { columns: this.props.columns, data: this.props.data, bootstrap4: this.props.bootstrap4, - setDependencyModules: this.setDependencyModules + setDependencyModules: this.setDependencyModules, + registerExposedAPI: this.registerExposedAPI }; if (this.props.search) { baseProps.search = { diff --git a/packages/react-bootstrap-table2-toolkit/src/op/csv.js b/packages/react-bootstrap-table2-toolkit/src/op/csv.js index b3803aa..8c6ced3 100644 --- a/packages/react-bootstrap-table2-toolkit/src/op/csv.js +++ b/packages/react-bootstrap-table2-toolkit/src/op/csv.js @@ -4,13 +4,14 @@ const csvDefaultOptions = { fileName: 'spreadsheet.csv', separator: ',', ignoreHeader: false, - noAutoBOM: true + noAutoBOM: true, + exportAll: true }; export default Base => class CSVOperation extends Base { handleExportCSV = () => { - const { columns, data, exportCSV } = this.props; + const { columns, exportCSV } = this.props; const meta = getMetaInfo(columns); const options = exportCSV === true ? csvDefaultOptions : @@ -18,6 +19,8 @@ export default Base => ...csvDefaultOptions, ...exportCSV }; + + const data = options.exportAll ? this.props.data : this.getData(); const content = transform(data, meta, this._.get, options); save(content, options); } diff --git a/packages/react-bootstrap-table2-toolkit/src/search/context.js b/packages/react-bootstrap-table2-toolkit/src/search/context.js index 150612b..67935bf 100644 --- a/packages/react-bootstrap-table2-toolkit/src/search/context.js +++ b/packages/react-bootstrap-table2-toolkit/src/search/context.js @@ -20,25 +20,10 @@ export default (options = { searchText: PropTypes.string } - constructor(props) { - super(props); - this.needToSearch = true; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.searchText !== this.props.searchText) { - this.needToSearch = true; - } else { - this.needToSearch = false; - } - } - search() { const { data, columns } = this.props; let { searchText } = this.props; - if (!this.needToSearch) return data; - if (isRemoteSearch()) { handleRemoteSearchChange(searchText); return data; diff --git a/packages/react-bootstrap-table2-toolkit/statelessOp.js b/packages/react-bootstrap-table2-toolkit/statelessOp.js index b39287d..6c248c1 100644 --- a/packages/react-bootstrap-table2-toolkit/statelessOp.js +++ b/packages/react-bootstrap-table2-toolkit/statelessOp.js @@ -1,4 +1,10 @@ import Operation from './src/op'; export default Base => - class StatelessOperation extends Operation.csvOperation(Base) {}; + class StatelessOperation extends Operation.csvOperation(Base) { + registerExposedAPI = (...exposedFuncs) => { + exposedFuncs.forEach((func) => { + this[func.name] = func; + }); + } + }; diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index 9afaab0..50ec912 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -15,6 +15,15 @@ class BootstrapTable extends PropsBaseResolver(Component) { constructor(props) { super(props); this.validateProps(); + if (props.registerExposedAPI) { + const getData = () => this.getData(); + props.registerExposedAPI(getData); + } + } + + // Exposed APIs + getData = () => { + return this.props.data; } render() { @@ -161,7 +170,7 @@ BootstrapTable.propTypes = { }), onRowExpand: PropTypes.func, onAllRowExpand: PropTypes.func, - isAnyExpands: PropTypes.func, + isAnyExpands: PropTypes.bool, rowStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), rowEvents: PropTypes.object, rowClasses: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), diff --git a/packages/react-bootstrap-table2/src/cell.js b/packages/react-bootstrap-table2/src/cell.js index bfd61d5..db32d05 100644 --- a/packages/react-bootstrap-table2/src/cell.js +++ b/packages/react-bootstrap-table2/src/cell.js @@ -10,6 +10,25 @@ class Cell extends Component { this.handleEditingCell = this.handleEditingCell.bind(this); } + shouldComponentUpdate(nextProps) { + const shouldUpdate = + _.get(this.props.row, this.props.column.dataField) + !== _.get(nextProps.row, nextProps.column.dataField) || + this.props.column.hidden !== nextProps.column.hidden || + this.props.rowIndex !== nextProps.rowIndex || + this.props.columnIndex !== nextProps.columnIndex || + this.props.className !== nextProps.className || + this.props.title !== nextProps.title || + this.props.editable !== nextProps.editable || + this.props.clickToEdit !== nextProps.clickToEdit || + this.props.dbclickToEdit !== nextProps.dbclickToEdit || + !_.isEqual(this.props.style, nextProps.style) || + !_.isEqual(this.props.column.formatExtraData, nextProps.column.formatExtraData) || + !_.isEqual(this.props.column.events, nextProps.column.events) || + !_.isEqual(this.props.column.attrs, nextProps.column.attrs); + return shouldUpdate; + } + handleEditingCell(e) { const { column, onStart, rowIndex, columnIndex, clickToEdit, dbclickToEdit } = this.props; const { events } = column; @@ -33,62 +52,32 @@ class Cell extends Component { rowIndex, column, columnIndex, + onStart, editable, clickToEdit, - dbclickToEdit + dbclickToEdit, + ...rest } = this.props; const { dataField, formatter, - formatExtraData, - style, - classes, - title, - events, - align, - attrs + formatExtraData } = column; - let cellTitle; - let cellStyle = {}; + const attrs = { ...rest }; 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 (cellClasses) cellAttrs.className = cellClasses; - - if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle; if (clickToEdit && editable) { - cellAttrs.onClick = this.handleEditingCell; + attrs.onClick = this.handleEditingCell; } else if (dbclickToEdit && editable) { - cellAttrs.onDoubleClick = this.handleEditingCell; + attrs.onDoubleClick = this.handleEditingCell; } + return ( - + { typeof content === 'boolean' ? `${content}` : content } ); diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js index 1196716..e3254b5 100644 --- a/packages/react-bootstrap-table2/src/row.js +++ b/packages/react-bootstrap-table2/src/row.js @@ -102,6 +102,45 @@ class Row extends eventDelegater(Component) { /> ); } + // render cell + let cellTitle; + let cellStyle = {}; + const cellAttrs = { + ..._.isFunction(column.attrs) + ? column.attrs(content, row, rowIndex, index) + : column.attrs, + ...column.events + }; + + const cellClasses = _.isFunction(column.classes) + ? column.classes(content, row, rowIndex, index) + : column.classes; + + if (column.style) { + cellStyle = _.isFunction(column.style) + ? column.style(content, row, rowIndex, index) + : column.style; + cellStyle = Object.assign({}, cellStyle) || {}; + } + + + if (column.title) { + cellTitle = _.isFunction(column.title) + ? column.title(content, row, rowIndex, index) + : content; + cellAttrs.title = cellTitle; + } + + if (column.align) { + cellStyle.textAlign = + _.isFunction(column.align) + ? column.align(content, row, rowIndex, index) + : column.align; + } + + if (cellClasses) cellAttrs.className = cellClasses; + if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle; + return ( ); } diff --git a/packages/react-bootstrap-table2/src/utils.js b/packages/react-bootstrap-table2/src/utils.js index 16572b2..ac8dd5a 100644 --- a/packages/react-bootstrap-table2/src/utils.js +++ b/packages/react-bootstrap-table2/src/utils.js @@ -1,6 +1,7 @@ /* eslint no-empty: 0 */ /* eslint no-param-reassign: 0 */ /* eslint prefer-rest-params: 0 */ +import _ from 'underscore'; function splitNested(str) { return [str] @@ -38,22 +39,8 @@ function set(target, field, value, safe = false) { }, target); } -function isFunction(obj) { - return obj && (typeof obj === 'function'); -} - -/** - * Checks if `value` is the Object. the `Object` except `Function` and `Array.` - * - * @param {*} obj - The value gonna check - */ -function isObject(obj) { - const type = typeof obj; - return obj !== null && type === 'object' && obj.constructor === Object; -} - function isEmptyObject(obj) { - if (!isObject(obj)) return false; + if (!_.isObject(obj)) return false; const hasOwnProperty = Object.prototype.hasOwnProperty; const keys = Object.keys(obj); @@ -91,18 +78,9 @@ function debounce(func, wait, immediate) { timeout = setTimeout(later, wait || 0); if (callNow) { - func.appy(this, arguments); + func.apply(this, arguments); } }; } -export default { - get, - set, - isFunction, - isObject, - isEmptyObject, - isDefined, - sleep, - debounce -}; +export default Object.assign(_, { get, set, isDefined, isEmptyObject, sleep, debounce }); diff --git a/packages/react-bootstrap-table2/test/bootstrap-table.test.js b/packages/react-bootstrap-table2/test/bootstrap-table.test.js index 4991018..23986a8 100644 --- a/packages/react-bootstrap-table2/test/bootstrap-table.test.js +++ b/packages/react-bootstrap-table2/test/bootstrap-table.test.js @@ -46,6 +46,40 @@ describe('BootstrapTable', () => { }); }); + describe('getData', () => { + let instance; + + beforeEach(() => { + wrapper = shallow( + ); + instance = wrapper.instance(); + }); + + it('should return props.data', () => { + expect(instance.getData()).toEqual(data); + }); + }); + + describe('when props.registerExposedAPI is defined', () => { + const registerExposedAPI = jest.fn(); + beforeEach(() => { + registerExposedAPI.mockClear(); + wrapper = shallow( + + ); + }); + + it('should call props.registerExposedAPI correctly', () => { + expect(registerExposedAPI).toHaveBeenCalledTimes(1); + expect(registerExposedAPI.mock.calls[0][0].name).toEqual('getData'); + }); + }); + describe('when props.classes was defined', () => { const classes = 'foo'; diff --git a/packages/react-bootstrap-table2/test/cell.test.js b/packages/react-bootstrap-table2/test/cell.test.js index 1695240..86bf393 100644 --- a/packages/react-bootstrap-table2/test/cell.test.js +++ b/packages/react-bootstrap-table2/test/cell.test.js @@ -79,362 +79,6 @@ describe('Cell', () => { }); }); - describe('when column.style prop is defined', () => { - let column; - const columnIndex = 1; - const rowIndex = 1; - - beforeEach(() => { - column = { - dataField: 'id', - text: 'ID' - }; - }); - - describe('when style is an object', () => { - beforeEach(() => { - column.style = { backgroundColor: 'red' }; - wrapper = shallow( - ); - }); - - it('should render successfully', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('style')).toEqual(column.style); - }); - }); - - describe('when style is a function', () => { - const returnStyle = { backgroundColor: 'red' }; - let styleCallBack; - - beforeEach(() => { - styleCallBack = sinon.stub() - .withArgs(row[column.dataField], row, rowIndex, columnIndex) - .returns(returnStyle); - column.style = styleCallBack; - wrapper = shallow( - ); - }); - - afterEach(() => { styleCallBack.reset(); }); - - it('should render successfully', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('style')).toEqual(returnStyle); - }); - - it('should call custom style function correctly', () => { - expect(styleCallBack.callCount).toBe(1); - expect( - styleCallBack.calledWith(row[column.dataField], row, rowIndex, columnIndex) - ).toBe(true); - }); - }); - }); - - describe('when column.classes prop is defined', () => { - let column; - const columnIndex = 1; - const rowIndex = 1; - - beforeEach(() => { - column = { - dataField: 'id', - text: 'ID' - }; - }); - - describe('when classes is an object', () => { - beforeEach(() => { - column.classes = 'td-test-class'; - wrapper = shallow( - ); - }); - - it('should render successfully', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.hasClass(column.classes)).toBe(true); - }); - }); - - describe('when classes is a function', () => { - const returnClasses = 'td-test-class'; - let classesCallBack; - - beforeEach(() => { - classesCallBack = sinon.stub() - .withArgs(row[column.dataField], row, rowIndex, columnIndex) - .returns(returnClasses); - column.classes = classesCallBack; - wrapper = shallow( - ); - }); - - afterEach(() => { classesCallBack.reset(); }); - - it('should render successfully', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.hasClass(returnClasses)).toBe(true); - }); - - it('should call custom classes function correctly', () => { - expect(classesCallBack.callCount).toBe(1); - expect( - classesCallBack.calledWith(row[column.dataField], row, rowIndex, columnIndex) - ).toBe(true); - }); - }); - }); - - describe('when column.title prop is defined', () => { - let column; - const columnIndex = 1; - const rowIndex = 1; - - beforeEach(() => { - column = { - dataField: 'id', - text: 'ID' - }; - }); - - describe('when title is boolean', () => { - beforeEach(() => { - column.title = true; - wrapper = shallow( - ); - }); - - it('should render title as cell value as default', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('title')).toEqual(row[column.dataField]); - }); - }); - - describe('when title is custom function', () => { - const customTitle = 'test_title'; - let titleCallBack; - - beforeEach(() => { - titleCallBack = sinon.stub() - .withArgs(row[column.dataField], row, rowIndex, columnIndex) - .returns(customTitle); - column.title = titleCallBack; - wrapper = shallow( - ); - }); - - it('should render title correctly by custom title function', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('title')).toBe(customTitle); - }); - - it('should call custom title function correctly', () => { - expect(titleCallBack.callCount).toBe(1); - expect( - titleCallBack.calledWith(row[column.dataField], row, rowIndex, columnIndex) - ).toBe(true); - }); - }); - }); - - describe('when column.events prop is defined', () => { - let column; - const columnIndex = 1; - const rowIndex = 1; - - beforeEach(() => { - column = { - dataField: 'id', - text: 'ID', - events: { - onClick: sinon.stub() - } - }; - - wrapper = shallow( - ); - }); - - it('should attachs DOM event successfully', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('onClick')).toBeDefined(); - }); - - it('event hook should be called when triggering', () => { - wrapper.find('td').simulate('click'); - expect(column.events.onClick.callCount).toBe(1); - }); - }); - - describe('when column.align prop is defined', () => { - let column; - const columnIndex = 1; - const rowIndex = 1; - - beforeEach(() => { - column = { - dataField: 'id', - text: 'ID' - }; - }); - - describe('when align is string', () => { - beforeEach(() => { - column.align = 'center'; - wrapper = shallow( - ); - }); - - it('should render style.textAlign correctly', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('style').textAlign).toEqual(column.align); - }); - }); - - describe('when align is custom function', () => { - const customAlign = 'center'; - let alignCallBack; - - beforeEach(() => { - alignCallBack = sinon.stub() - .withArgs(row[column.dataField], row, rowIndex, columnIndex) - .returns(customAlign); - column.align = alignCallBack; - wrapper = shallow( - ); - }); - - it('should render style.textAlign correctly', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('style').textAlign).toEqual(customAlign); - }); - - it('should call custom headerAlign function correctly', () => { - expect(alignCallBack.callCount).toBe(1); - expect( - alignCallBack.calledWith(row[column.dataField], row, rowIndex, columnIndex) - ).toBe(true); - }); - }); - }); - - describe('when column.attrs prop is defined', () => { - let column; - const columnIndex = 1; - const rowIndex = 1; - - beforeEach(() => { - column = { - dataField: 'id', - text: 'ID' - }; - }); - - describe('when attrs is an object', () => { - it('should render column.attrs correctly', () => { - column.attrs = { - 'data-test': 'test', - title: 'title', - className: 'attrs-class', - style: { - backgroundColor: 'attrs-style-test', - display: 'none', - textAlign: 'right' - } - }; - wrapper = shallow( - ); - - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('data-test')).toEqual(column.attrs['data-test']); - expect(wrapper.find('td').prop('title')).toEqual(column.attrs.title); - expect(wrapper.hasClass(column.attrs.className)).toBe(true); - expect(wrapper.find('td').prop('style')).toEqual(column.attrs.style); - expect(wrapper.find('td').prop('style').textAlign).toEqual(column.attrs.style.textAlign); - }); - - describe('when column.title prop is defined', () => { - it('attrs.title should be overwrited', () => { - column.title = true; - column.attrs = { title: 'title' }; - - wrapper = shallow( - ); - - expect(wrapper.find('td').prop('title')).toEqual(row[column.dataField]); - }); - }); - - describe('when column.classes prop is defined', () => { - it('attrs.class should be overwrited', () => { - column.classes = 'td-test-class'; - column.attrs = { className: 'attrs-class' }; - - wrapper = shallow( - ); - - expect(wrapper.hasClass(column.classes)).toBe(true); - }); - }); - - describe('when column.style prop is defined', () => { - it('attrs.style should be overwrited', () => { - column.style = { backgroundColor: 'red' }; - column.attrs = { style: { backgroundColor: 'attrs-style-test' } }; - - wrapper = shallow( - ); - - expect(wrapper.find('td').prop('style')).toEqual(column.style); - }); - }); - - describe('when column.align prop is defined', () => { - it('attrs.style.textAlign should be overwrited', () => { - column.align = 'center'; - column.attrs = { style: { textAlign: 'right' } }; - - wrapper = shallow( - ); - - expect(wrapper.find('td').prop('style').textAlign).toEqual(column.align); - }); - }); - }); - - describe('when attrs is custom function', () => { - let attrsCallBack; - const customAttrs = { - title: 'title', - 'data-test': 'test' - }; - - beforeEach(() => { - attrsCallBack = sinon.stub() - .withArgs(row[column.dataField], row, rowIndex, columnIndex) - .returns(customAttrs); - column.attrs = attrsCallBack; - wrapper = shallow( - ); - }); - - it('should render style.attrs correctly', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find('td').prop('data-test')).toEqual(customAttrs['data-test']); - expect(wrapper.find('td').prop('title')).toEqual(customAttrs.title); - }); - - it('should call custom attrs function correctly', () => { - expect(attrsCallBack.callCount).toBe(1); - expect( - attrsCallBack.calledWith(row[column.dataField], row, rowIndex, columnIndex) - ).toBe(true); - }); - }); - }); - describe('when editable prop is true', () => { let onStartCallBack; const rowIndex = 1; @@ -528,4 +172,259 @@ describe('Cell', () => { }); }); }); + + describe('shouldComponentUpdate', () => { + let props; + let nextProps; + + describe('when content is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, row: { id: 1, name: 'CDE' } }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when column.hidden is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, column: { ...column, hidden: true } }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.rowIndex is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, rowIndex: 2 }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.columnIndex is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, columnIndex: 2 }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.className is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column, + className: 'test' + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, className: null }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.title is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column, + title: 'test' + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, title: '123' }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.title is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, editable: true }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.clickToEdit is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, clickToEdit: true }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.dbclickToEdit is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, dbclickToEdit: true }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when props.style is change', () => { + const column = { dataField: 'name', text: 'Product Name' }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column, + style: {} + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, style: { color: 'red' } }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when column.formatExtraData is change', () => { + const column = { dataField: 'name', text: 'Product Name', formatExtraData: { a: 1 } }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, column: { ...column, formatExtraData: { b: 2 } } }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when column.events is change', () => { + const column = { dataField: 'name', text: 'Product Name', events: { a: jest.fn() } }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, column: { ...column, events: { b: jest.fn() } } }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + + describe('when column.attrs is change', () => { + const column = { dataField: 'name', text: 'Product Name', attrs: { 'data-att': 1 } }; + beforeEach(() => { + props = { + row, + columnIndex: 1, + rowIndex: 1, + column + }; + wrapper = shallow( + ); + }); + + it('should return true', () => { + nextProps = { ...props, column: { ...column, attrs: null } }; + expect(wrapper.instance().shouldComponentUpdate(nextProps)).toBe(true); + }); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index 117a24f..621f140 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -8,7 +8,7 @@ import Const from '../src/const'; import SelectionCell from '../src//row-selection/selection-cell'; import mockBodyResolvedProps from './test-helpers/mock/body-resolved-props'; -const defaultColumns = [{ +let defaultColumns = [{ dataField: 'id', text: 'ID' }, { @@ -31,6 +31,19 @@ describe('Row', () => { price: 1000 }; + beforeEach(() => { + defaultColumns = [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }, { + dataField: 'price', + text: 'Price' + }]; + }); + describe('simplest row', () => { beforeEach(() => { wrapper = shallow( @@ -502,7 +515,7 @@ describe('Row', () => { }); }); - describe('when cloumn.hidden is true', () => { + describe('when column.hidden is true', () => { beforeEach(() => { const newColumns = [{ dataField: 'id', @@ -870,4 +883,445 @@ describe('Row', () => { }); }); }); + + describe('when column.style prop is defined', () => { + let columns; + const columnIndex = 1; + + beforeEach(() => { + columns = [...defaultColumns]; + }); + + describe('when style is an object', () => { + beforeEach(() => { + columns[columnIndex].style = { backgroundColor: 'red' }; + wrapper = shallow( + + ); + }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.style).toEqual(columns[columnIndex].style); + }); + }); + + describe('when style is a function', () => { + const returnStyle = { backgroundColor: 'red' }; + let styleCallBack; + + beforeEach(() => { + styleCallBack = sinon.stub().returns(returnStyle); + columns[columnIndex].style = styleCallBack; + wrapper = shallow( + + ); + }); + + afterEach(() => { styleCallBack.reset(); }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.style).toEqual(returnStyle); + }); + + it('should call custom style function correctly', () => { + expect(styleCallBack.callCount).toBe(1); + expect( + styleCallBack.calledWith(row[columns[columnIndex].dataField], row, rowIndex, columnIndex) + ).toBe(true); + }); + }); + }); + + describe('when column.classes prop is defined', () => { + let columns; + const columnIndex = 1; + + beforeEach(() => { + columns = [...defaultColumns]; + }); + + describe('when classes is an object', () => { + beforeEach(() => { + columns[columnIndex].classes = 'td-test-class'; + wrapper = shallow( + + ); + }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.className) + .toEqual(columns[columnIndex].classes); + }); + }); + + describe('when classes is a function', () => { + const returnClasses = 'td-test-class'; + let classesCallBack; + + beforeEach(() => { + classesCallBack = sinon.stub().returns(returnClasses); + columns[columnIndex].classes = classesCallBack; + wrapper = shallow( + + ); + }); + + afterEach(() => { classesCallBack.reset(); }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.className).toEqual(returnClasses); + }); + + it('should call custom classes function correctly', () => { + expect(classesCallBack.callCount).toBe(1); + expect( + classesCallBack.calledWith( + row[columns[columnIndex].dataField], row, rowIndex, columnIndex) + ).toBe(true); + }); + }); + }); + + describe('when column.title prop is defined', () => { + let columns; + const columnIndex = 1; + + beforeEach(() => { + columns = [...defaultColumns]; + }); + + describe('when title is an string', () => { + beforeEach(() => { + columns[columnIndex].title = true; + wrapper = shallow( + + ); + }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.title) + .toEqual(row[columns[columnIndex].dataField]); + }); + }); + + describe('when title is a function', () => { + const returnTitle = 'test title'; + let titleCallBack; + + beforeEach(() => { + titleCallBack = sinon.stub().returns(returnTitle); + columns[columnIndex].title = titleCallBack; + wrapper = shallow( + + ); + }); + + afterEach(() => { titleCallBack.reset(); }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.title).toEqual(returnTitle); + }); + + it('should call custom title function correctly', () => { + expect(titleCallBack.callCount).toBe(1); + expect( + titleCallBack.calledWith( + row[columns[columnIndex].dataField], row, rowIndex, columnIndex) + ).toBe(true); + }); + }); + }); + + describe('when column.events prop is defined', () => { + let columns; + const columnIndex = 1; + + beforeEach(() => { + columns = [...defaultColumns]; + columns[columnIndex].events = { + onClick: sinon.stub() + }; + + wrapper = shallow( + + ); + }); + + it('should attachs DOM event successfully', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.onClick).toBeDefined(); + }); + }); + + describe('when column.align prop is defined', () => { + let columns; + const columnIndex = 1; + + beforeEach(() => { + columns = [...defaultColumns]; + }); + + describe('when align is a string', () => { + beforeEach(() => { + columns[columnIndex].align = 'right'; + wrapper = shallow( + + ); + }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.style.textAlign) + .toEqual(columns[columnIndex].align); + }); + }); + + describe('when align is a function', () => { + const returnAlign = 'right'; + let alignCallBack; + + beforeEach(() => { + alignCallBack = sinon.stub().returns(returnAlign); + columns[columnIndex].align = alignCallBack; + wrapper = shallow( + + ); + }); + + afterEach(() => { alignCallBack.reset(); }); + + it('should render Cell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props.style.textAlign).toEqual(returnAlign); + }); + + it('should call custom align function correctly', () => { + expect(alignCallBack.callCount).toBe(1); + expect( + alignCallBack.calledWith(row[columns[columnIndex].dataField], row, rowIndex, columnIndex) + ).toBe(true); + }); + }); + }); + + describe('when column.attrs prop is defined', () => { + let columns; + const columnIndex = 1; + + beforeEach(() => { + columns = [...defaultColumns]; + }); + + describe('when attrs is an object', () => { + it('should render Cell correctly', () => { + columns[columnIndex].attrs = { + 'data-test': 'test', + title: 'title', + className: 'attrs-class', + style: { + backgroundColor: 'attrs-style-test', + display: 'none', + textAlign: 'right' + } + }; + + wrapper = shallow( + + ); + + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props['data-test']) + .toEqual(columns[columnIndex].attrs['data-test']); + expect(wrapper.find(Cell).get(columnIndex).props.title) + .toEqual(columns[columnIndex].attrs.title); + expect(wrapper.find(Cell).get(columnIndex).props.className) + .toEqual(columns[columnIndex].attrs.className); + expect(wrapper.find(Cell).get(columnIndex).props.style) + .toEqual(columns[columnIndex].attrs.style); + }); + + describe('when column.title prop is defined', () => { + it('attrs.title should be overwrited', () => { + columns[columnIndex].title = true; + columns[columnIndex].attrs = { title: 'title' }; + + wrapper = shallow( + + ); + + expect(wrapper.find(Cell).get(columnIndex).props.title) + .toEqual(row[columns[columnIndex].dataField]); + }); + }); + + describe('when column.classes prop is defined', () => { + it('attrs.className should be overwrited', () => { + columns[columnIndex].classes = 'td-test-class'; + columns[columnIndex].attrs = { className: 'attrs-class' }; + + wrapper = shallow( + + ); + + expect(wrapper.find(Cell).get(columnIndex).props.className) + .toEqual(columns[columnIndex].classes); + }); + }); + + describe('when column.style prop is defined', () => { + it('attrs.style should be overwrited', () => { + columns[columnIndex].style = { backgroundColor: 'red' }; + columns[columnIndex].attrs = { style: { backgroundColor: 'attrs-style-test' } }; + + wrapper = shallow( + + ); + + expect(wrapper.find(Cell).get(columnIndex).props.style) + .toEqual(columns[columnIndex].style); + }); + }); + + describe('when column.align prop is defined', () => { + it('attrs.style.textAlign should be overwrited', () => { + columns[columnIndex].align = 'center'; + columns[columnIndex].attrs = { style: { textAlign: 'right' } }; + + wrapper = shallow( + + ); + + expect(wrapper.find(Cell).get(columnIndex).props.style.textAlign) + .toEqual(columns[columnIndex].align); + }); + }); + }); + + describe('when attrs is custom function', () => { + let attrsCallBack; + const customAttrs = { + 'data-test': 'test', + title: 'title' + }; + + beforeEach(() => { + attrsCallBack = sinon.stub().returns(customAttrs); + columns[columnIndex].attrs = attrsCallBack; + wrapper = shallow( + + ); + }); + + it('should render style.attrs correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(Cell).get(columnIndex).props['data-test']) + .toEqual(customAttrs['data-test']); + expect(wrapper.find(Cell).get(columnIndex).props.title) + .toEqual(customAttrs.title); + }); + + it('should call custom attrs function correctly', () => { + expect(attrsCallBack.callCount).toBe(1); + expect( + attrsCallBack.calledWith(row[columns[columnIndex].dataField], row, rowIndex, columnIndex) + ).toBe(true); + }); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/utils.test.js b/packages/react-bootstrap-table2/test/utils.test.js index 2aa43b1..89d9ff1 100644 --- a/packages/react-bootstrap-table2/test/utils.test.js +++ b/packages/react-bootstrap-table2/test/utils.test.js @@ -58,32 +58,6 @@ describe('Utils', () => { }); }); - describe('isObject', () => { - describe('when given Object', () => { - it('should return true', () => { - expect(_.isObject({})).toBe(true); - }); - }); - - describe('when given Function', () => { - it('should return false', () => { - expect(_.isObject(() => 'test')).toBe(false); - }); - }); - - describe('when given Array', () => { - it('should return false', () => { - expect(_.isObject([])).toBe(false); - }); - }); - - describe('when given null', () => { - it('should return false', () => { - expect(_.isObject(null)).toBe(false); - }); - }); - }); - describe('isEmptyObject', () => { describe('when given empty Object', () => { it('should return true', () => { @@ -98,14 +72,14 @@ describe('Utils', () => { }); describe('when given Function', () => { - it('should return false', () => { - expect(_.isEmptyObject(() => 'test')).toBe(false); + it('should return true', () => { + expect(_.isEmptyObject(() => 'test')).toBe(true); }); }); describe('when given Array', () => { - it('should return false', () => { - expect(_.isEmptyObject([])).toBe(false); + it('should return true', () => { + expect(_.isEmptyObject([])).toBe(true); }); }); diff --git a/yarn.lock b/yarn.lock index ff2fc0d..4329d62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8286,6 +8286,10 @@ unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" +underscore@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + underscore@~1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"