refine remote mode on filter and pagination

This commit is contained in:
AllenFang 2017-12-24 16:02:05 +08:00
parent e14c596b8c
commit c4eb2f835f
7 changed files with 315 additions and 68 deletions

View File

@ -1,12 +1,16 @@
/* eslint react/prop-types: 0 */
import { Component } from 'react';
import PropTypes from 'prop-types';
import { filters } from './filter';
import { LIKE } from './comparison';
export default class FilterWrapper extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
columns: PropTypes.array.isRequired,
baseElement: PropTypes.func.isRequired,
onRemoteFilterChange: PropTypes.func.isRequired,
// refactoring later
_: PropTypes.object.isRequired
}
@ -16,28 +20,51 @@ export default class FilterWrapper extends Component {
this.onFilter = this.onFilter.bind(this);
}
componentWillReceiveProps() {
this.setState(() => ({ isDataChanged: false }));
componentWillReceiveProps(nextProps) {
// consider to use lodash.isEqual
if (JSON.stringify(this.state.currFilters) !== JSON.stringify(nextProps.store.filters)) {
this.setState(() => ({ isDataChanged: true, currFilters: nextProps.store.filters }));
} else {
this.setState(() => ({ isDataChanged: false }));
}
}
onFilter(column, filterVal, filterType) {
const { store, columns, _ } = this.props;
const { currFilters } = this.state;
const { store, columns, _, onRemoteFilterChange } = this.props;
const currFilters = Object.assign({}, this.state.currFilters);
const { dataField, filter } = column;
if (!_.isDefined(filterVal) || filterVal === '') {
delete currFilters[dataField];
} else {
const { comparator } = filter.props;
const { comparator = LIKE } = filter.props;
currFilters[dataField] = { filterVal, filterType, comparator };
}
store.filters = currFilters;
if (this.isRemote() || this.isPaginationRemote()) {
onRemoteFilterChange(this.isPaginationRemote());
// when remote filtering is enable, dont set currFilters state
// in the componentWillReceiveProps, it's the key point that we can know the filter is changed
return;
}
store.filteredData = filters(store, columns, _)(currFilters);
store.filtering = Object.keys(currFilters).length > 0;
this.setState(() => ({ currFilters, isDataChanged: true }));
}
// refactoring later
isRemote() {
const { remote } = this.props;
return remote === true || (typeof remote === 'object' && remote.filter);
}
// refactoring later
isPaginationRemote() {
const { remote } = this.props;
return remote === true || (typeof remote === 'object' && remote.pagination);
}
render() {
return this.props.baseElement({
...this.props,

View File

@ -1,4 +1,5 @@
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import _ from 'react-bootstrap-table2/src/utils';
@ -20,7 +21,11 @@ for (let i = 0; i < 20; i += 1) {
describe('Wrapper', () => {
let wrapper;
let instance;
const onRemoteFilterChangeCB = sinon.stub();
afterEach(() => {
onRemoteFilterChangeCB.reset();
});
const createTableProps = () => {
const tableProps = {
@ -40,7 +45,8 @@ describe('Wrapper', () => {
data,
filter: filter(),
_,
store: new Store('id')
store: new Store('id'),
onRemoteFilterChange: onRemoteFilterChangeCB
};
tableProps.store.data = data;
return tableProps;
@ -84,13 +90,28 @@ describe('Wrapper', () => {
describe('componentWillReceiveProps', () => {
let nextProps;
beforeEach(() => {
nextProps = createTableProps();
instance.componentWillReceiveProps(nextProps);
describe('when props.store.filters is same as current state.currFilters', () => {
beforeEach(() => {
nextProps = createTableProps();
instance.componentWillReceiveProps(nextProps);
});
it('should setting isDataChanged as false (Temporary solution)', () => {
expect(instance.state.isDataChanged).toBeFalsy();
});
});
it('should setting isDataChanged as false always(Temporary solution)', () => {
expect(instance.state.isDataChanged).toBeFalsy();
describe('when props.store.filters is different from current state.currFilters', () => {
beforeEach(() => {
nextProps = createTableProps();
nextProps.store.filters = { price: { filterVal: 20, filterType: FILTER_TYPE.TEXT } };
instance.componentWillReceiveProps(nextProps);
});
it('should setting states correctly', () => {
expect(instance.state.isDataChanged).toBeTruthy();
expect(instance.state.currFilters).toBe(nextProps.store.filters);
});
});
});
@ -126,7 +147,7 @@ describe('Wrapper', () => {
it('should setting store object correctly', () => {
instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
expect(props.store.filters).toEqual(instance.state.currFilters);
});
it('should setting state correctly', () => {
@ -136,30 +157,54 @@ describe('Wrapper', () => {
});
});
describe('when remote filter is enabled', () => {
const filterVal = '3';
beforeEach(() => {
props = createTableProps();
props.remote = { filter: true };
createFilterWrapper(props);
instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT);
});
it('should not setting store object correctly', () => {
expect(props.store.filters).not.toEqual(instance.state.currFilters);
});
it('should not setting state', () => {
expect(instance.state.isDataChanged).toBeFalsy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(0);
});
it('should calling props.onRemoteFilterChange correctly', () => {
expect(onRemoteFilterChangeCB.calledOnce).toBeTruthy();
});
});
describe('combination', () => {
it('should setting store object correctly', () => {
instance.onFilter(props.columns[1], '3', FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
expect(props.store.filters).toEqual(instance.state.currFilters);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(1);
instance.onFilter(props.columns[1], '2', FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
expect(props.store.filters).toEqual(instance.state.currFilters);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(1);
instance.onFilter(props.columns[2], '2', FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
expect(props.store.filters).toEqual(instance.state.currFilters);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(2);
instance.onFilter(props.columns[2], '', FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
expect(props.store.filters).toEqual(instance.state.currFilters);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(1);
instance.onFilter(props.columns[1], '', FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeFalsy();
expect(props.store.filters).toEqual(instance.state.currFilters);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(0);
});

View File

@ -11,7 +11,8 @@ import { getByCurrPage } from './page';
class PaginationWrapper extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
baseElement: PropTypes.func.isRequired
baseElement: PropTypes.func.isRequired,
onRemotePageChange: PropTypes.func.isRequired
}
constructor(props) {
@ -43,14 +44,15 @@ class PaginationWrapper extends Component {
}
this.state = { currPage, currSizePerPage };
this.saveToStore(currPage, currSizePerPage);
}
componentWillReceiveProps(nextProps) {
let needNewState = false;
let { currPage, currSizePerPage } = this.state;
const { page, sizePerPage, pageStartIndex } = nextProps.pagination.options;
const { page, sizePerPage, pageStartIndex, onPageChange } = nextProps.pagination.options;
if (typeof page !== 'undefined') { // user defined page
if (typeof page !== 'undefined' && currPage !== page) { // user defined page
currPage = page;
needNewState = true;
} else if (nextProps.isDataChanged) { // user didn't defined page but data change
@ -63,7 +65,19 @@ class PaginationWrapper extends Component {
needNewState = true;
}
if (needNewState) this.setState(() => ({ currPage, currSizePerPage }));
this.saveToStore(currPage, currSizePerPage);
if (needNewState) {
if (onPageChange) {
onPageChange(currPage, currSizePerPage);
}
this.setState(() => ({ currPage, currSizePerPage }));
}
}
saveToStore(page, sizePerPage) {
this.props.store.page = page;
this.props.store.sizePerPage = sizePerPage;
}
isRemote() {
@ -74,11 +88,13 @@ class PaginationWrapper extends Component {
handleChangePage(currPage) {
const { currSizePerPage } = this.state;
const { pagination: { options }, onRemotePageChange } = this.props;
this.saveToStore(currPage, currSizePerPage);
if (options.onPageChange) {
options.onPageChange(currPage, currSizePerPage);
}
if (this.isRemote()) {
onRemotePageChange(currPage, currSizePerPage);
onRemotePageChange();
return;
}
this.setState(() => {
@ -90,11 +106,13 @@ class PaginationWrapper extends Component {
handleChangeSizePerPage(currSizePerPage, currPage) {
const { pagination: { options }, onRemotePageChange } = this.props;
this.saveToStore(currPage, currSizePerPage);
if (options.onSizePerPageChange) {
options.onSizePerPageChange(currSizePerPage, currPage);
}
if (this.isRemote()) {
onRemotePageChange(currPage, currSizePerPage);
onRemotePageChange();
return;
}
this.setState(() => {

View File

@ -21,6 +21,11 @@ for (let i = 0; i < 100; i += 1) {
describe('Wrapper', () => {
let wrapper;
let instance;
const onRemotePageChangeCB = sinon.stub();
afterEach(() => {
onRemotePageChangeCB.reset();
});
const createTableProps = (props = {}) => {
const tableProps = {
@ -34,7 +39,8 @@ describe('Wrapper', () => {
}],
data,
pagination: paginator(props.options),
store: new Store('id')
store: new Store('id'),
onRemotePageChange: onRemotePageChangeCB
};
tableProps.store.data = data;
return tableProps;
@ -69,6 +75,11 @@ describe('Wrapper', () => {
expect(instance.state.currSizePerPage).toEqual(Const.SIZE_PER_PAGE_LIST[0]);
});
it('should saving page and sizePerPage to store correctly', () => {
expect(props.store.page).toBe(instance.state.currPage);
expect(props.store.sizePerPage).toBe(instance.state.currSizePerPage);
});
it('should rendering BootstraTable correctly', () => {
const table = wrapper.find(BootstrapTable);
expect(table.length).toBe(1);
@ -105,10 +116,19 @@ describe('Wrapper', () => {
nextProps = createTableProps();
});
it('should setting currPage state correctly by options.page', () => {
nextProps.pagination.options.page = 2;
instance.componentWillReceiveProps(nextProps);
expect(instance.state.currPage).toEqual(nextProps.pagination.options.page);
describe('when options.page is existing', () => {
beforeEach(() => {
nextProps.pagination.options.page = 2;
instance.componentWillReceiveProps(nextProps);
});
it('should setting currPage state correctly', () => {
expect(instance.state.currPage).toEqual(nextProps.pagination.options.page);
});
it('should saving store.page correctly', () => {
expect(props.store.page).toEqual(instance.state.currPage);
});
});
it('should not setting currPage state if options.page not existing', () => {
@ -117,10 +137,19 @@ describe('Wrapper', () => {
expect(instance.state.currPage).toBe(currPage);
});
it('should setting currSizePerPage state correctly by options.sizePerPage', () => {
nextProps.pagination.options.sizePerPage = 20;
instance.componentWillReceiveProps(nextProps);
expect(instance.state.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage);
describe('when options.sizePerPage is existing', () => {
beforeEach(() => {
nextProps.pagination.options.sizePerPage = 20;
instance.componentWillReceiveProps(nextProps);
});
it('should setting currSizePerPage state correctly', () => {
expect(instance.state.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage);
});
it('should saving store.sizePerPage correctly', () => {
expect(props.store.sizePerPage).toEqual(instance.state.currSizePerPage);
});
});
it('should not setting currSizePerPage state if options.sizePerPage not existing', () => {
@ -129,17 +158,35 @@ describe('Wrapper', () => {
expect(instance.state.currSizePerPage).toBe(currSizePerPage);
});
it('should setting currPage state when nextProps.isDataChanged is true', () => {
nextProps.isDataChanged = true;
instance.componentWillReceiveProps(nextProps);
expect(instance.state.currPage).toBe(Const.PAGE_START_INDEX);
describe('when nextProps.isDataChanged is true', () => {
beforeEach(() => {
nextProps.isDataChanged = true;
instance.componentWillReceiveProps(nextProps);
});
it('should setting currPage state correctly', () => {
expect(instance.state.currPage).toBe(Const.PAGE_START_INDEX);
});
it('should saving store.page correctly', () => {
expect(props.store.page).toEqual(instance.state.currPage);
});
});
it('should setting currPage state when nextProps.isDataChanged is true and options.pageStartIndex is existing', () => {
nextProps.isDataChanged = true;
nextProps.pagination.options.pageStartIndex = 0;
instance.componentWillReceiveProps(nextProps);
expect(instance.state.currPage).toBe(nextProps.pagination.options.pageStartIndex);
describe('when nextProps.isDataChanged is true and options.pageStartIndex is existing', () => {
beforeEach(() => {
nextProps.isDataChanged = true;
nextProps.pagination.options.pageStartIndex = 0;
instance.componentWillReceiveProps(nextProps);
});
it('should setting currPage state correctly', () => {
expect(instance.state.currPage).toBe(nextProps.pagination.options.pageStartIndex);
});
it('should saving store.page correctly', () => {
expect(props.store.page).toEqual(instance.state.currPage);
});
});
});
});
@ -448,11 +495,16 @@ describe('Wrapper', () => {
expect(onPageChange.calledWith(newPage, instance.state.currSizePerPage)).toBeTruthy();
});
it('should saving page and sizePerPage to store correctly', () => {
expect(props.store.page).toBe(newPage);
expect(props.store.sizePerPage).toBe(instance.state.currSizePerPage);
});
describe('when pagination remote is enable', () => {
beforeEach(() => {
props.remote = true;
props.onRemotePageChange = sinon.stub();
createPaginationWrapper(props, false);
onRemotePageChangeCB.reset();
instance.handleChangePage(newPage);
});
@ -460,10 +512,8 @@ describe('Wrapper', () => {
expect(instance.state.currPage).not.toEqual(newPage);
});
it('should calling options.onRemotePageChange correctly', () => {
expect(props.onRemotePageChange.calledOnce).toBeTruthy();
expect(props.onRemotePageChange.calledWith(
newPage, instance.state.currSizePerPage)).toBeTruthy();
it('should calling props.onRemotePageChange correctly', () => {
expect(onRemotePageChangeCB.calledOnce).toBeTruthy();
});
});
});
@ -492,11 +542,16 @@ describe('Wrapper', () => {
expect(onSizePerPageChange.calledWith(newSizePerPage, newPage)).toBeTruthy();
});
it('should saving page and sizePerPage to store correctly', () => {
expect(props.store.page).toBe(newPage);
expect(props.store.sizePerPage).toBe(newSizePerPage);
});
describe('when pagination remote is enable', () => {
beforeEach(() => {
props.remote = true;
props.onRemotePageChange = sinon.stub();
createPaginationWrapper(props, false);
onRemotePageChangeCB.reset();
instance.handleChangeSizePerPage(newSizePerPage, newPage);
});
@ -505,9 +560,8 @@ describe('Wrapper', () => {
expect(instance.state.currSizePerPage).not.toEqual(newSizePerPage);
});
it('should calling options.onRemotePageChange correctly', () => {
expect(props.onRemotePageChange.calledOnce).toBeTruthy();
expect(props.onRemotePageChange.calledWith(newPage, newSizePerPage)).toBeTruthy();
it('should calling props.onRemotePageChange correctly', () => {
expect(onRemotePageChangeCB.calledOnce).toBeTruthy();
});
});
});

View File

@ -20,16 +20,35 @@ const withDataStore = Base =>
this.store = new Store(props.keyField);
this.store.data = props.data;
this.handleUpdateCell = this.handleUpdateCell.bind(this);
this.onRemotePageChange = this.onRemotePageChange.bind(this);
this.handleRemotePageChange = this.handleRemotePageChange.bind(this);
this.handleRemoteFilterChange = this.handleRemoteFilterChange.bind(this);
}
componentWillReceiveProps(nextProps) {
this.store.data = nextProps.data;
}
onRemotePageChange(page, sizePerPage) {
const newState = { page, sizePerPage };
this.props.onTableChange(newState);
getNewestState(state = {}) {
return {
page: this.store.page,
sizePerPage: this.store.sizePerPage,
filters: this.store.filters,
...state
};
}
handleRemotePageChange() {
this.props.onTableChange('pagination', this.getNewestState());
}
// refactoring later for isRemotePagination
handleRemoteFilterChange(isRemotePagination) {
const newState = {};
if (isRemotePagination) {
const options = this.props.pagination.options || {};
newState.page = _.isDefined(options.pageStartIndex) ? options.pageStartIndex : 1;
}
this.props.onTableChange('filter', this.getNewestState(newState));
}
handleUpdateCell(rowId, dataField, newValue) {
@ -72,13 +91,17 @@ const withDataStore = Base =>
} else if (this.props.selectRow) {
return wrapWithSelection(baseProps);
} else if (this.props.filter) {
return wrapWithFilter(baseProps);
return wrapWithFilter({
...baseProps,
onRemoteFilterChange: this.handleRemoteFilterChange,
onRemotePageChange: this.handleRemotePageChange
});
} else if (this.props.columns.filter(col => col.sort).length > 0) {
return wrapWithSort(baseProps);
} else if (this.props.pagination) {
return wrapWithPagination({
...baseProps,
onRemotePageChange: this.onRemotePageChange
onRemotePageChange: this.handleRemotePageChange
});
}

View File

@ -11,7 +11,9 @@ export default class Store {
this._sortOrder = undefined;
this._sortField = undefined;
this._selected = [];
this._filtering = false;
this._filters = {};
this._page = undefined;
this._sizePerPage = undefined;
}
edit(rowId, dataField, newValue) {
@ -30,13 +32,13 @@ export default class Store {
}
get data() {
if (this._filtering) {
if (Object.keys(this._filters).length > 0) {
return this._filteredData;
}
return this._data;
}
set data(data) {
if (this._filtering) {
if (Object.keys(this._filters).length > 0) {
this._filteredData = data;
} else {
this._data = (data ? JSON.parse(JSON.stringify(data)) : []);
@ -52,12 +54,18 @@ export default class Store {
get sortOrder() { return this._sortOrder; }
set sortOrder(sortOrder) { this._sortOrder = sortOrder; }
get page() { return this._page; }
set page(page) { this._page = page; }
get sizePerPage() { return this._sizePerPage; }
set sizePerPage(sizePerPage) { this._sizePerPage = sizePerPage; }
get sortField() { return this._sortField; }
set sortField(sortField) { this._sortField = sortField; }
get selected() { return this._selected; }
set selected(selected) { this._selected = selected; }
get filtering() { return this._filtering; }
set filtering(filtering) { this._filtering = filtering; }
get filters() { return this._filters; }
set filters(filters) { this._filters = filters; }
}

View File

@ -202,9 +202,7 @@ describe('withDataStore', () => {
});
});
describe('onRemotePageChange', () => {
const page = 2;
const sizePerPage = 25;
describe('handleRemotePageChange', () => {
const onTableChangeCallBack = sinon.stub();
beforeEach(() => {
@ -216,12 +214,86 @@ describe('withDataStore', () => {
onTableChange={ onTableChangeCallBack }
/>
);
wrapper.instance().onRemotePageChange(page, sizePerPage);
wrapper.instance().handleRemotePageChange();
});
it('should calling onTableChange correctly', () => {
expect(onTableChangeCallBack.calledOnce).toBeTruthy();
expect(onTableChangeCallBack.calledWith({ page, sizePerPage })).toBeTruthy();
expect(onTableChangeCallBack.calledWith('pagination', wrapper.instance().getNewestState())).toBeTruthy();
});
});
describe('handleRemoteFilterChange', () => {
const onTableChangeCallBack = sinon.stub();
beforeEach(() => {
onTableChangeCallBack.reset();
wrapper = shallow(
<BootstrapTable
keyField={ keyField }
data={ data }
columns={ columns }
onTableChange={ onTableChangeCallBack }
/>
);
});
describe('when isRemotePagination argument is false', () => {
beforeEach(() => {
wrapper.instance().handleRemoteFilterChange(false);
});
it('should calling onTableChange correctly', () => {
expect(onTableChangeCallBack.calledOnce).toBeTruthy();
expect(onTableChangeCallBack.calledWith('filter', wrapper.instance().getNewestState())).toBeTruthy();
});
});
describe('when isRemotePagination argument is false', () => {
describe('and pagination.options.pageStartIndex is defined', () => {
const options = { pageStartIndex: 0 };
beforeEach(() => {
wrapper = shallow(
<BootstrapTable
keyField={ keyField }
data={ data }
columns={ columns }
pagination={ { options, PaginationWrapper: () => {} } }
onTableChange={ onTableChangeCallBack }
/>
);
wrapper.instance().handleRemoteFilterChange(true);
});
it('should calling onTableChange correctly', () => {
expect(onTableChangeCallBack.calledOnce).toBeTruthy();
const newState = wrapper.instance().getNewestState();
newState.page = options.pageStartIndex;
expect(onTableChangeCallBack.calledWith('filter', newState)).toBeTruthy();
});
});
describe('and pagination.options.pageStartIndex is not defined', () => {
beforeEach(() => {
wrapper = shallow(
<BootstrapTable
keyField={ keyField }
data={ data }
columns={ columns }
pagination={ { PaginationWrapper: () => {} } }
onTableChange={ onTableChangeCallBack }
/>
);
wrapper.instance().handleRemoteFilterChange(true);
});
it('should calling onTableChange correctly', () => {
expect(onTableChangeCallBack.calledOnce).toBeTruthy();
const newState = wrapper.instance().getNewestState();
newState.page = 1;
expect(onTableChangeCallBack.calledWith('filter', newState)).toBeTruthy();
});
});
});
});
});