diff --git a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js
index 8ebc72a..25f09d8 100644
--- a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js
+++ b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js
@@ -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'],
diff --git a/packages/react-bootstrap-table2-example/package.json b/packages/react-bootstrap-table2-example/package.json
index a726ef5..419f884 100644
--- a/packages/react-bootstrap-table2-example/package.json
+++ b/packages/react-bootstrap-table2-example/package.json
@@ -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",
diff --git a/packages/react-bootstrap-table2-filter/src/components/text.js b/packages/react-bootstrap-table2-filter/src/components/text.js
index 48ab658..1c102c3 100644
--- a/packages/react-bootstrap-table2-filter/src/components/text.js
+++ b/packages/react-bootstrap-table2-filter/src/components/text.js
@@ -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 (
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 = {
diff --git a/packages/react-bootstrap-table2-filter/test/components/text.test.js b/packages/react-bootstrap-table2-filter/test/components/text.test.js
new file mode 100644
index 0000000..4a9c282
--- /dev/null
+++ b/packages/react-bootstrap-table2-filter/test/components/text.test.js
@@ -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(
+
+ );
+ 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(
+
+ );
+ 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(
+
+ );
+ 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(
+
+ );
+ 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(
+
+ );
+ 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(
+
+ );
+ 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(
+
+ );
+ 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(
+
+ );
+ 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);
+ });
+ });
+});
diff --git a/packages/react-bootstrap-table2-filter/test/filter.test.js b/packages/react-bootstrap-table2-filter/test/filter.test.js
new file mode 100644
index 0000000..7f55a55
--- /dev/null
+++ b/packages/react-bootstrap-table2-filter/test/filter.test.js
@@ -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);
+ });
+ });
+ });
+});
diff --git a/packages/react-bootstrap-table2-filter/test/wrapper.test.js b/packages/react-bootstrap-table2-filter/test/wrapper.test.js
new file mode 100644
index 0000000..c877c62
--- /dev/null
+++ b/packages/react-bootstrap-table2-filter/test/wrapper.test.js
@@ -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 => ();
+
+ const createFilterWrapper = (props, renderFragment = true) => {
+ wrapper = shallow();
+ instance = wrapper.instance();
+ if (renderFragment) {
+ const fragment = instance.render();
+ wrapper = shallow(
{ fragment }
);
+ }
+ };
+
+ 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);
+ });
+ });
+ });
+});