add react-bootstrap-table2-filter

This commit is contained in:
AllenFang 2017-12-13 22:54:59 +08:00
parent ba93a6ce9c
commit 00185b80ca
6 changed files with 446 additions and 12 deletions

View File

@ -3,6 +3,7 @@ const path = require('path');
const sourcePath = path.join(__dirname, '../../react-bootstrap-table2/src');
const paginationSourcePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/src');
const overlaySourcePath = path.join(__dirname, '../../react-bootstrap-table2-overlay/src');
const filterSourcePath = path.join(__dirname, '../../react-bootstrap-table2-filter/src');
const sourceStylePath = path.join(__dirname, '../../react-bootstrap-table2/style');
const paginationStylePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/style');
const storyPath = path.join(__dirname, '../stories');
@ -26,7 +27,7 @@ const loaders = [{
test: /\.js?$/,
use: ['babel-loader'],
exclude: /node_modules/,
include: [sourcePath, paginationSourcePath, overlaySourcePath, storyPath],
include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, storyPath],
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader'],

View File

@ -19,7 +19,8 @@
"bootstrap": "^3.3.7",
"react-bootstrap-table2": "0.0.1",
"react-bootstrap-table2-paginator": "0.0.1",
"react-bootstrap-table2-overlay": "0.0.1"
"react-bootstrap-table2-overlay": "0.0.1",
"react-bootstrap-table2-filter": "0.0.1"
},
"devDependencies": {
"@storybook/addon-console": "^1.0.0",

View File

@ -1,5 +1,5 @@
/* eslint react/require-default-props: 0 */
/* eslint react/no-unused-prop-types: 0 */
/* eslint react/prop-types: 0 */
/* eslint no-return-assign: 0 */
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
@ -11,6 +11,7 @@ class TextFilter extends Component {
constructor(props) {
super(props);
this.filter = this.filter.bind(this);
this.handleClick = this.handleClick.bind(this);
this.timeout = null;
this.state = {
value: props.defaultValue
@ -30,14 +31,12 @@ class TextFilter extends Component {
}
componentWillUnmount() {
clearTimeout(this.timeout);
this.cleanTimer();
}
filter(e) {
e.stopPropagation();
if (this.timeout) {
clearTimeout(this.timeout);
}
this.cleanTimer();
const filterValue = e.target.value;
this.setState(() => ({ value: filterValue }));
this.timeout = setTimeout(() => {
@ -45,6 +44,12 @@ class TextFilter extends Component {
}, this.props.delay);
}
cleanTimer() {
if (this.timeout) {
clearTimeout(this.timeout);
}
}
cleanFiltered() {
const value = this.props.defaultValue;
this.setState(() => ({ value }));
@ -56,17 +61,25 @@ class TextFilter extends Component {
this.props.onFilter(this.props.column, filterText, FILTER_TYPE.TEXT);
}
handleClick(e) {
e.stopPropagation();
if (this.props.onClick) {
this.props.onClick(e);
}
}
render() {
const { placeholder, column: { text }, style } = this.props;
const { placeholder, column: { text }, style, className, onFilter, ...rest } = this.props;
// stopPropagation for onClick event is try to prevent sort was triggered.
return (
<input
{ ...rest }
ref={ n => this.input = n }
type="text"
className="filter text-filter form-control"
className={ `filter text-filter form-control ${className}` }
style={ style }
onChange={ this.filter }
onClick={ e => e.stopPropagation() }
onClick={ this.handleClick }
placeholder={ placeholder || `Enter ${text}...` }
value={ this.state.value }
/>
@ -76,12 +89,13 @@ class TextFilter extends Component {
TextFilter.propTypes = {
onFilter: PropTypes.func.isRequired,
column: PropTypes.object.isRequired,
comparator: PropTypes.oneOf([LIKE, EQ]),
defaultValue: PropTypes.string,
delay: PropTypes.number,
placeholder: PropTypes.string,
column: PropTypes.object,
style: PropTypes.object
style: PropTypes.object,
className: PropTypes.string
};
TextFilter.defaultProps = {

View File

@ -0,0 +1,190 @@
import 'jsdom-global/register';
import React from 'react';
import sinon from 'sinon';
import { mount } from 'enzyme';
import TextFilter from '../../src/components/text';
import { FILTER_TYPE } from '../../src/const';
jest.useFakeTimers();
describe('Text Filter', () => {
let wrapper;
let instance;
const onFilter = sinon.stub();
const column = {
dataField: 'price',
text: 'Price'
};
afterEach(() => {
onFilter.reset();
});
describe('initialization', () => {
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } />
);
instance = wrapper.instance();
});
it('should have correct state', () => {
expect(instance.state.value).toEqual(instance.props.defaultValue);
});
it('should rendering component successfully', () => {
expect(wrapper).toHaveLength(1);
expect(wrapper.find('input[type="text"]')).toHaveLength(1);
expect(instance.input.getAttribute('placeholder')).toEqual(`Enter ${column.text}...`);
});
});
describe('when defaultValue is defined', () => {
const defaultValue = '123';
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } defaultValue={ defaultValue } />
);
instance = wrapper.instance();
});
it('should have correct state', () => {
expect(instance.state.value).toEqual(defaultValue);
});
it('should rendering component successfully', () => {
expect(wrapper).toHaveLength(1);
expect(instance.input.value).toEqual(defaultValue);
});
it('should calling onFilter on componentDidMount', () => {
expect(onFilter.calledOnce).toBeTruthy();
expect(onFilter.calledWith(column, defaultValue, FILTER_TYPE.TEXT)).toBeTruthy();
});
});
describe('when placeholder is defined', () => {
const placeholder = 'test';
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } placeholder={ placeholder } />
);
instance = wrapper.instance();
});
it('should rendering component successfully', () => {
expect(wrapper).toHaveLength(1);
expect(instance.input.getAttribute('placeholder')).toEqual(placeholder);
});
});
describe('when style is defined', () => {
const style = { backgroundColor: 'red' };
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } style={ style } />
);
instance = wrapper.instance();
});
it('should rendering component successfully', () => {
expect(wrapper).toHaveLength(1);
expect(wrapper.find('input').prop('style')).toEqual(style);
});
});
describe('componentWillReceiveProps', () => {
const nextDefaultValue = 'tester';
const nextProps = {
onFilter,
column,
defaultValue: nextDefaultValue
};
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } />
);
instance = wrapper.instance();
instance.componentWillReceiveProps(nextProps);
});
it('should setting state correctly when props.defaultValue is changed', () => {
expect(instance.state.value).toEqual(nextDefaultValue);
});
it('should calling onFilter correctly when props.defaultValue is changed', () => {
expect(onFilter.calledOnce).toBeTruthy();
expect(onFilter.calledWith(column, nextDefaultValue, FILTER_TYPE.TEXT)).toBeTruthy();
});
});
describe('cleanFiltered', () => {
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } />
);
instance = wrapper.instance();
instance.cleanFiltered();
});
it('should setting state correctly', () => {
expect(instance.state.value).toEqual(instance.props.defaultValue);
});
it('should calling onFilter correctly', () => {
expect(onFilter.calledOnce).toBeTruthy();
expect(onFilter.calledWith(
column, instance.props.defaultValue, FILTER_TYPE.TEXT)).toBeTruthy();
});
});
describe('applyFilter', () => {
const filterText = 'test';
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } />
);
instance = wrapper.instance();
instance.applyFilter(filterText);
});
it('should setting state correctly', () => {
expect(instance.state.value).toEqual(filterText);
});
it('should calling onFilter correctly', () => {
expect(onFilter.calledOnce).toBeTruthy();
expect(onFilter.calledWith(column, filterText, FILTER_TYPE.TEXT)).toBeTruthy();
});
});
describe('filter', () => {
const event = { stopPropagation: sinon.stub(), target: { value: 'tester' } };
beforeEach(() => {
wrapper = mount(
<TextFilter onFilter={ onFilter } column={ column } />
);
instance = wrapper.instance();
instance.filter(event);
});
afterEach(() => {
setTimeout.mockClear();
});
it('should calling e.stopPropagation', () => {
expect(event.stopPropagation.calledOnce).toBeTruthy();
});
it('should setting state correctly', () => {
expect(instance.state.value).toEqual(event.target.value);
});
it('should calling setTimeout correctly', () => {
expect(setTimeout.mock.calls).toHaveLength(1);
expect(setTimeout.mock.calls[0]).toHaveLength(2);
expect(setTimeout.mock.calls[0][1]).toEqual(instance.props.delay);
});
});
});

View File

@ -0,0 +1,60 @@
import _ from 'react-bootstrap-table2/src/utils';
import Store from 'react-bootstrap-table2/src/store';
import { filters } from '../src/filter';
import { FILTER_TYPE } from '../src/const';
import { LIKE, EQ } from '../src/comparison';
const data = [];
for (let i = 0; i < 20; i += 1) {
data.push({
id: i,
name: `itme name ${i}`,
price: 200 + i
});
}
describe('filter', () => {
let store;
let filterFn;
let currFilters;
beforeEach(() => {
store = new Store('id');
store.data = data;
currFilters = {};
});
describe('text filter', () => {
beforeEach(() => {
filterFn = filters(store, _);
});
describe(`when default comparator is ${LIKE}`, () => {
it('should returning correct result', () => {
currFilters.name = {
filterVal: '3',
filterType: FILTER_TYPE.TEXT
};
const result = filterFn(currFilters);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
});
});
describe(`when default comparator is ${EQ}`, () => {
it('should returning correct result', () => {
currFilters.name = {
filterVal: 'itme name 3',
filterType: FILTER_TYPE.TEXT,
comparator: EQ
};
const result = filterFn(currFilters);
expect(result).toBeDefined();
expect(result).toHaveLength(1);
});
});
});
});

View File

@ -0,0 +1,168 @@
import React from 'react';
import { shallow } from 'enzyme';
import _ from 'react-bootstrap-table2/src/utils';
import BootstrapTable from 'react-bootstrap-table2/src/bootstrap-table';
import Store from 'react-bootstrap-table2/src/store';
import filter, { textFilter } from '../src';
import FilterWrapper from '../src/wrapper';
import { FILTER_TYPE } from '../src/const';
const data = [];
for (let i = 0; i < 20; i += 1) {
data.push({
id: i,
name: `itme name ${i}`,
price: 200 + i
});
}
describe('Wrapper', () => {
let wrapper;
let instance;
const createTableProps = () => {
const tableProps = {
keyField: 'id',
columns: [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'name',
text: 'Name',
filter: textFilter()
}, {
dataField: 'price',
text: 'Price',
filter: textFilter()
}],
data,
filter: filter(),
_,
store: new Store('id')
};
tableProps.store.data = data;
return tableProps;
};
const pureTable = props => (<BootstrapTable { ...props } />);
const createFilterWrapper = (props, renderFragment = true) => {
wrapper = shallow(<FilterWrapper { ...props } baseElement={ pureTable } />);
instance = wrapper.instance();
if (renderFragment) {
const fragment = instance.render();
wrapper = shallow(<div>{ fragment }</div>);
}
};
describe('default filter wrapper', () => {
const props = createTableProps();
beforeEach(() => {
createFilterWrapper(props);
});
it('should rendering correctly', () => {
expect(wrapper.length).toBe(1);
});
it('should initializing state correctly', () => {
expect(instance.state.isDataChanged).toBeFalsy();
expect(instance.state.currFilters).toEqual({});
});
it('should rendering BootstraTable correctly', () => {
const table = wrapper.find(BootstrapTable);
expect(table.length).toBe(1);
expect(table.prop('onFilter')).toBeDefined();
expect(table.prop('isDataChanged')).toEqual(instance.state.isDataChanged);
});
});
describe('componentWillReceiveProps', () => {
let nextProps;
beforeEach(() => {
nextProps = createTableProps();
instance.componentWillReceiveProps(nextProps);
});
it('should setting isDataChanged as false always(Temporary solution)', () => {
expect(instance.state.isDataChanged).toBeFalsy();
});
});
describe('onFilter', () => {
let props;
beforeEach(() => {
props = createTableProps();
createFilterWrapper(props);
});
describe('when filterVal is empty or undefined', () => {
const filterVals = ['', undefined];
it('should setting store object correctly', () => {
filterVals.forEach((filterVal) => {
instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeFalsy();
});
});
it('should setting state correctly', () => {
filterVals.forEach((filterVal) => {
instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(0);
});
});
});
describe('when filterVal is existing', () => {
const filterVal = '3';
it('should setting store object correctly', () => {
instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
});
it('should setting state correctly', () => {
instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT);
expect(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(1);
});
});
describe('combination', () => {
it('should setting store object correctly', () => {
instance.onFilter(props.columns[1], '3', FILTER_TYPE.TEXT);
expect(props.store.filtering).toBeTruthy();
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(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(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(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(instance.state.isDataChanged).toBeTruthy();
expect(Object.keys(instance.state.currFilters)).toHaveLength(0);
});
});
});
});