Implement TextFilter
This commit is contained in:
Allen 2017-12-23 14:00:37 +08:00 committed by GitHub
commit 80b1ac3370
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1284 additions and 52 deletions

View File

@ -22,6 +22,7 @@
* [rowEvents](#rowEvents)
* [defaultSorted](#defaultSorted)
* [pagination](#pagination)
* [filter](#filter)
* [onTableChange](#onTableChange)
### <a name='keyField'>keyField(**required**) - [String]</a>
@ -198,6 +199,33 @@ paginator({
})
```
### <a name='filter'>filter - [Object]</a>
`filter` allow user to filter data by column. However, filter funcitonality is separated from core of `react-bootstrap-table2` so that you are suppose to install `react-bootstrap-table2-filter` firstly.
```sh
$ npm install react-bootstrap-table2-filter --save
```
After installation of `react-bootstrap-table2-filter`, you can configure filter on `react-bootstrap-table2` easily:
```js
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
// omit...
const columns = [ {
dataField: 'id',
text: 'Production ID'
}, {
dataField: 'name',
text: 'Production Name',
filter: textFilter() // apply text filter
}, {
dataField: 'price',
text: 'Production Price'
} ];
<BootstrapTable data={ data } columns={ columns } filter={ filterFactory() } />
```
### <a name='onTableChange'>onTableChange - [Function]</a>
This callback function will be called when [`remote`](#remote) enabled only.

View File

@ -31,20 +31,22 @@ Available properties in a column object:
* [validator](#validator)
* [editCellStyle](#editCellStyle)
* [editCellClasses](#editCellClasses)
* [filter](#filter)
* [filterValue](#filterValue)
Following is a most simplest and basic usage:
```js
const rows = [ { id: 1, name: '...', price: '102' } ];
const columns = [ {
dataField: id,
text: Production ID
dataField: 'id',
text: 'Production ID'
}, {
dataField: name,
text: Production Name
dataField: 'name',
text: 'Production Name'
}, {
dataField: price,
text: Production Price
dataField: 'price',
text: 'Production Price'
}
];
```
@ -525,4 +527,24 @@ Or take a callback function
// it is suppose to return a string
}
}
```
```
## <a name='filter'>column.filter - [Object]</a>
Configure `column.filter` will able to setup a column level filter on the header column. Currently, `react-bootstrap-table2` support following filters:
* Text(`textFilter`)
We have a quick example to show you how to use `column.filter`:
```
import { textFilter } from 'react-bootstrap-table2-filter';
// omit...
{
dataField: 'price',
text: 'Product Price',
filter: textFilter()
}
```
For some reason of simple customization, `react-bootstrap-table2` allow you to pass some props to filter factory function. Please check [here](https://github.com/react-bootstrap-table/react-bootstrap-table2/tree/master/packages/react-bootstrap-table2-filter/README.md) for more detail tutorial.

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

@ -0,0 +1,74 @@
/* eslint no-unused-vars: 0 */
import React from 'react';
import BootstrapTable from 'react-bootstrap-table2';
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
import Code from 'components/common/code-block';
import { jobsGenerator } from 'utils/common';
const jobs = jobsGenerator(5);
const owners = ['Allen', 'Bob', 'Cat'];
const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money'];
const columns = [{
dataField: 'id',
text: 'Job ID'
}, {
dataField: 'name',
text: 'Job Name',
filter: textFilter()
}, {
dataField: 'owner',
text: 'Job Owner',
filter: textFilter(),
formatter: (cell, row) => owners[cell],
filterValue: (cell, row) => owners[cell]
}, {
dataField: 'type',
text: 'Job Type',
filter: textFilter(),
formatter: (cell, row) => types[cell],
filterValue: (cell, row) => types[cell]
}];
const sourceCode = `\
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
const owners = ['Allen', 'Bob', 'Cat'];
const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money'];
const columns = [{
dataField: 'id',
text: 'Job ID'
}, {
dataField: 'name',
text: 'Job Name',
filter: textFilter()
}, {
dataField: 'owner',
text: 'Job Owner',
filter: textFilter(),
formatter: (cell, row) => owners[cell],
filterValue: (cell, row) => owners[cell]
}, {
dataField: 'type',
text: 'Job Type',
filter: textFilter(),
filterValue: (cell, row) => types[cell]
}];
// shape of job: { id: 0, name: 'Job name 0', owner: 1, type: 3 }
<BootstrapTable keyField='id' data={ jobs } columns={ columns } filter={ filterFactory() } />
`;
export default () => (
<div>
<BootstrapTable
keyField="id"
data={ jobs }
columns={ columns }
filter={ filterFactory() }
/>
<Code>{ sourceCode }</Code>
</div>
);

View File

@ -0,0 +1,68 @@
/* eslint no-console: 0 */
import React from 'react';
import BootstrapTable from 'react-bootstrap-table2';
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator(8);
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name',
filter: textFilter()
}, {
dataField: 'price',
text: 'Product Price',
filter: textFilter({
delay: 1000, // default is 500ms
style: {
backgroundColor: 'yellow'
},
className: 'test-classname',
placeholder: 'Custom PlaceHolder',
onClick: e => console.log(e)
})
}];
const sourceCode = `\
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name',
filter: textFilter()
}, {
dataField: 'price',
text: 'Product Price',
filter: textFilter({
delay: 1000, // default is 500ms
style: {
backgroundColor: 'yellow'
},
className: 'test-classname',
placeholder: 'Custom PlaceHolder',
onClick: e => console.log(e)
})
}];
<BootstrapTable keyField='id' data={ products } columns={ columns } filter={ filterFactory() } />
`;
export default () => (
<div>
<BootstrapTable
keyField="id"
data={ products }
columns={ columns }
filter={ filterFactory() }
/>
<Code>{ sourceCode }</Code>
</div>
);

View File

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

View File

@ -0,0 +1,56 @@
import React from 'react';
import BootstrapTable from 'react-bootstrap-table2';
import filterFactory, { textFilter, Comparator } from 'react-bootstrap-table2-filter';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator(8);
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name',
filter: textFilter({
comparator: Comparator.EQ // default is Comparator.LIKE
})
}, {
dataField: 'price',
text: 'Product Price',
filter: textFilter()
}];
const sourceCode = `\
import filterFactory, { textFilter, Comparator } from 'react-bootstrap-table2-filter';
const columns = [{
dataField: 'id',
text: 'Product ID'
}, {
dataField: 'name',
text: 'Product Name',
filter: textFilter({
comparator: Comparator.EQ
})
}, {
dataField: 'price',
text: 'Product Price',
filter: textFilter()
}];
<BootstrapTable keyField='id' data={ products } columns={ columns } filter={ filterFactory() } />
`;
export default () => (
<div>
<h3>Product Name filter apply Equal Comparator</h3>
<BootstrapTable
keyField="id"
data={ products }
columns={ columns }
filter={ filterFactory() }
/>
<Code>{ sourceCode }</Code>
</div>
);

View File

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

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

@ -20,4 +20,12 @@ export const productsGenerator = (quantity = 5, callback) => {
);
};
export const jobsGenerator = (quantity = 5) =>
Array.from({ length: quantity }, (value, index) => ({
id: index,
name: `Job name ${index}`,
owner: Math.floor(Math.random() * 3),
type: Math.floor(Math.random() * 5)
}));
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

View File

@ -32,6 +32,13 @@ import HeaderColumnClassTable from 'examples/header-columns/column-class-table';
import HeaderColumnStyleTable from 'examples/header-columns/column-style-table';
import HeaderColumnAttrsTable from 'examples/header-columns/column-attrs-table';
// column filter
import TextFilter from 'examples/column-filter/text-filter';
import TextFilterWithDefaultValue from 'examples/column-filter/text-filter-default-value';
import TextFilterComparator from 'examples/column-filter/text-filter-eq-comparator';
import CustomTextFilter from 'examples/column-filter/custom-text-filter';
import CustomFilterValue from 'examples/column-filter/custom-filter-value';
// work on rows
import RowStyleTable from 'examples/rows/row-style';
import RowClassTable from 'examples/rows/row-class';
@ -121,6 +128,14 @@ storiesOf('Work on Header Columns', module)
.add('Customize Column Style', () => <HeaderColumnStyleTable />)
.add('Customize Column HTML attribute', () => <HeaderColumnAttrsTable />);
storiesOf('Column Filter', module)
.add('Text Filter', () => <TextFilter />)
.add('Text Filter with Default Value', () => <TextFilterWithDefaultValue />)
.add('Text Filter with Comparator', () => <TextFilterComparator />)
.add('Custom Text Filter', () => <CustomTextFilter />)
// add another filter type example right here.
.add('Custom Filter Value', () => <CustomFilterValue />);
storiesOf('Work on Rows', module)
.add('Customize Row Style', () => <RowStyleTable />)
.add('Customize Row Class', () => <RowClassTable />)

View File

@ -0,0 +1,40 @@
# react-bootstrap-table2-filter
## Filters
* Text (`textFilter`)
You can get all of above filters via import and these filters are a factory function to create a individual filter instance.
In addition, for some simple customization reasons, these factory function allow to pass some props.
### Text Filter
```js
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
// omit...
const columns = [
..., {
dataField: 'price',
text: 'Product Price',
filter: textFilter()
}];
<BootstrapTable keyField='id' data={ products } columns={ columns } filter={ filterFactory() } />
```
Following we list all the availabe props for `textFilter` function:
```js
import { Comparator } from 'react-bootstrap-table2-filter';
// omit...
const customTextFilter = textFilter({
placeholder: 'My Custom PlaceHolder', // custom the input placeholder
style: { ... }, // your custom styles on input
className: 'my-custom-text-filter', // custom classname on input
defaultValue: 'test', // default filtering value
delay: 1000, // how long will trigger filtering after user typing, default is 500 ms
comparator: Comparator.EQ // default is Comparator.LIKE
});
```

View File

@ -0,0 +1,11 @@
{
"name": "react-bootstrap-table2-filter",
"version": "0.0.1",
"description": "it's the column filter addon for react-bootstrap-table2",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@ -0,0 +1,2 @@
export const LIKE = 'LIKE';
export const EQ = '=';

View File

@ -0,0 +1,107 @@
/* eslint react/require-default-props: 0 */
/* eslint react/prop-types: 0 */
/* eslint no-return-assign: 0 */
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
import { LIKE, EQ } from '../comparison';
import { FILTER_TYPE, FILTER_DELAY } from '../const';
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
};
}
componentDidMount() {
const defaultValue = this.input.value;
if (defaultValue) {
this.props.onFilter(this.props.column, defaultValue, FILTER_TYPE.TEXT);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.defaultValue !== this.props.defaultValue) {
this.applyFilter(nextProps.defaultValue);
}
}
componentWillUnmount() {
this.cleanTimer();
}
filter(e) {
e.stopPropagation();
this.cleanTimer();
const filterValue = e.target.value;
this.setState(() => ({ value: filterValue }));
this.timeout = setTimeout(() => {
this.props.onFilter(this.props.column, filterValue, FILTER_TYPE.TEXT);
}, this.props.delay);
}
cleanTimer() {
if (this.timeout) {
clearTimeout(this.timeout);
}
}
cleanFiltered() {
const value = this.props.defaultValue;
this.setState(() => ({ value }));
this.props.onFilter(this.props.column, value, FILTER_TYPE.TEXT);
}
applyFilter(filterText) {
this.setState(() => ({ value: filterText }));
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, 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}` }
style={ style }
onChange={ this.filter }
onClick={ this.handleClick }
placeholder={ placeholder || `Enter ${text}...` }
value={ this.state.value }
/>
);
}
}
TextFilter.propTypes = {
onFilter: PropTypes.func.isRequired,
column: PropTypes.object.isRequired,
comparator: PropTypes.oneOf([LIKE, EQ]),
defaultValue: PropTypes.string,
delay: PropTypes.number,
placeholder: PropTypes.string,
style: PropTypes.object,
className: PropTypes.string
};
TextFilter.defaultProps = {
delay: FILTER_DELAY,
defaultValue: ''
};
export default TextFilter;

View File

@ -0,0 +1,5 @@
export const FILTER_TYPE = {
TEXT: 'TEXT'
};
export const FILTER_DELAY = 500;

View File

@ -0,0 +1,45 @@
import { FILTER_TYPE } from './const';
import { LIKE, EQ } from './comparison';
export const filterByText = _ => (
data,
dataField,
{ filterVal, comparator = LIKE },
customFilterValue
) =>
data.filter((row) => {
let cell = _.get(row, dataField);
if (customFilterValue) {
cell = customFilterValue(cell, row);
}
const cellStr = _.isDefined(cell) ? cell.toString() : '';
if (comparator === EQ) {
return cellStr === filterVal;
}
return cellStr.indexOf(filterVal) > -1;
});
export const filterFactory = _ => (filterType) => {
let filterFn;
switch (filterType) {
case FILTER_TYPE.TEXT:
filterFn = filterByText(_);
break;
default:
filterFn = filterByText(_);
}
return filterFn;
};
export const filters = (store, columns, _) => (currFilters) => {
const factory = filterFactory(_);
let result = store.getAllData();
let filterFn;
Object.keys(currFilters).forEach((dataField) => {
const filterObj = currFilters[dataField];
filterFn = factory(filterObj.filterType);
const { filterValue } = columns.find(col => col.dataField === dataField);
result = filterFn(result, dataField, filterObj, filterValue);
});
return result;
};

View File

@ -0,0 +1,15 @@
import TextFilter from './components/text';
import FilterWrapper from './wrapper';
import * as Comparison from './comparison';
export default (options = {}) => ({
FilterWrapper,
options
});
export const Comparator = Comparison;
export const textFilter = (props = {}) => ({
Filter: TextFilter,
props
});

View File

@ -0,0 +1,49 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { filters } from './filter';
export default class FilterWrapper extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
columns: PropTypes.array.isRequired,
baseElement: PropTypes.func.isRequired,
_: PropTypes.object.isRequired
}
constructor(props) {
super(props);
this.state = { currFilters: {}, isDataChanged: false };
this.onFilter = this.onFilter.bind(this);
}
componentWillReceiveProps() {
this.setState(() => ({ isDataChanged: false }));
}
onFilter(column, filterVal, filterType) {
const { store, columns, _ } = this.props;
const { currFilters } = this.state;
const { dataField, filter } = column;
if (!_.isDefined(filterVal) || filterVal === '') {
delete currFilters[dataField];
} else {
const { comparator } = filter.props;
currFilters[dataField] = { filterVal, filterType, comparator };
}
store.filteredData = filters(store, columns, _)(currFilters);
store.filtering = Object.keys(currFilters).length > 0;
this.setState(() => ({ currFilters, isDataChanged: true }));
}
render() {
return this.props.baseElement({
...this.props,
key: 'table',
onFilter: this.onFilter,
isDataChanged: this.state.isDataChanged
});
}
}

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,94 @@
import sinon from 'sinon';
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;
let columns;
beforeEach(() => {
store = new Store('id');
store.data = data;
currFilters = {};
columns = [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'name',
text: 'Name'
}, {
dataField: 'price',
text: 'Price'
}];
});
describe('text filter', () => {
beforeEach(() => {
filterFn = filters(store, columns, _);
});
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);
});
});
describe('column.filterValue is defined', () => {
beforeEach(() => {
columns[1].filterValue = sinon.stub();
filterFn = filters(store, columns, _);
});
it('should calling custom filterValue callback correctly', () => {
currFilters.name = {
filterVal: '3',
filterType: FILTER_TYPE.TEXT
};
const result = filterFn(currFilters);
expect(result).toBeDefined();
expect(columns[1].filterValue.callCount).toBe(data.length);
const calls = columns[1].filterValue.getCalls();
calls.forEach((call, i) => {
expect(call.calledWith(data[i].name, data[i])).toBeTruthy();
});
});
});
});
});

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);
});
});
});
});

View File

@ -18,8 +18,7 @@ export default ExtendBase =>
return { totalPages, lastPage, dropdownOpen: false };
}
calculateTotalPage(sizePerPage = this.props.currSizePerPage) {
const { dataSize } = this.props;
calculateTotalPage(sizePerPage = this.props.currSizePerPage, dataSize = this.props.dataSize) {
return Math.ceil(dataSize / sizePerPage);
}

View File

@ -1,11 +1,12 @@
export const getByCurrPage = store => (page, sizePerPage, pageStartIndex) => {
const dataSize = store.data.length;
if (!dataSize) return [];
const getNormalizedPage = () => {
const offset = Math.abs(1 - pageStartIndex);
return page + offset;
};
const end = (getNormalizedPage() * sizePerPage) - 1;
const start = end - (sizePerPage - 1);
const dataSize = store.data.length;
const result = [];
for (let i = start; i <= end; i += 1) {

View File

@ -20,9 +20,8 @@ class Pagination extends pageResolver(Component) {
componentWillReceiveProps(nextProps) {
const { dataSize, currSizePerPage } = nextProps;
if (currSizePerPage !== this.props.currSizePerPage || dataSize !== this.props.dataSize) {
const totalPages = this.calculateTotalPage(currSizePerPage);
const totalPages = this.calculateTotalPage(currSizePerPage, dataSize);
const lastPage = this.calculateLastPage(totalPages);
this.setState({ totalPages, lastPage });
}

View File

@ -48,11 +48,16 @@ class PaginationWrapper extends Component {
componentWillReceiveProps(nextProps) {
let needNewState = false;
let { currPage, currSizePerPage } = this.state;
const { page, sizePerPage } = nextProps.pagination.options;
if (typeof page !== 'undefined') {
const { page, sizePerPage, pageStartIndex } = nextProps.pagination.options;
if (typeof page !== 'undefined') { // user defined page
currPage = page;
needNewState = true;
} else if (nextProps.isDataChanged) { // user didn't defined page but data change
currPage = typeof pageStartIndex !== 'undefined' ? pageStartIndex : Const.PAGE_START_INDEX;
needNewState = true;
}
if (typeof sizePerPage !== 'undefined') {
currSizePerPage = sizePerPage;
needNewState = true;

View File

@ -4,6 +4,18 @@ import { getByCurrPage } from '../src/page';
describe('Page Functions', () => {
let data;
let store;
const params = [
// [page, sizePerPage, pageStartIndex]
[1, 10, 1],
[1, 25, 1],
[1, 30, 1],
[3, 30, 1],
[4, 30, 1],
[10, 10, 1],
[0, 10, 0],
[1, 10, 0],
[9, 10, 0]
];
describe('getByCurrPage', () => {
beforeEach(() => {
@ -16,23 +28,20 @@ describe('Page Functions', () => {
});
it('should always return correct data', () => {
[
// [page, sizePerPage, pageStartIndex]
[1, 10, 1],
[1, 25, 1],
[1, 30, 1],
[3, 30, 1],
[4, 30, 1],
[10, 10, 1],
[0, 10, 0],
[1, 10, 0],
[9, 10, 0]
].forEach(([page, sizePerPage, pageStartIndex]) => {
params.forEach(([page, sizePerPage, pageStartIndex]) => {
const rows = getByCurrPage(store)(page, sizePerPage, pageStartIndex);
expect(rows).toBeDefined();
expect(Array.isArray(rows)).toBeTruthy();
expect(rows.every(row => !!row)).toBeTruthy();
});
});
it('should return empty array when store.data is empty', () => {
store.data = [];
params.forEach(([page, sizePerPage, pageStartIndex]) => {
const rows = getByCurrPage(store)(page, sizePerPage, pageStartIndex);
expect(rows).toHaveLength(0);
});
});
});
});

View File

@ -143,12 +143,13 @@ describe('Pagination', () => {
it('should setting correct state.totalPages', () => {
instance.componentWillReceiveProps(nextProps);
expect(instance.state.totalPages).toEqual(
instance.calculateTotalPage(nextProps.currSizePerPage));
instance.calculateTotalPage(nextProps.currSizePerPage, nextProps.dataSize));
});
it('should setting correct state.lastPage', () => {
instance.componentWillReceiveProps(nextProps);
const totalPages = instance.calculateTotalPage(nextProps.currSizePerPage);
const totalPages = instance.calculateTotalPage(
nextProps.currSizePerPage, nextProps.dataSize);
expect(instance.state.lastPage).toEqual(
instance.calculateLastPage(totalPages));
});

View File

@ -100,29 +100,47 @@ describe('Wrapper', () => {
});
describe('componentWillReceiveProps', () => {
it('should setting currPage state correclt by options.page', () => {
props.pagination.options.page = 2;
instance.componentWillReceiveProps(props);
expect(instance.state.currPage).toEqual(props.pagination.options.page);
let nextProps;
beforeEach(() => {
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);
});
it('should not setting currPage state if options.page not existing', () => {
const { currPage } = instance.state;
instance.componentWillReceiveProps(props);
instance.componentWillReceiveProps(nextProps);
expect(instance.state.currPage).toBe(currPage);
});
it('should setting currSizePerPage state correclt by options.sizePerPage', () => {
props.pagination.options.sizePerPage = 20;
instance.componentWillReceiveProps(props);
expect(instance.state.currSizePerPage).toEqual(props.pagination.options.sizePerPage);
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);
});
it('should not setting currSizePerPage state if options.sizePerPage not existing', () => {
const { currSizePerPage } = instance.state;
instance.componentWillReceiveProps(props);
instance.componentWillReceiveProps(nextProps);
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);
});
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);
});
});
});

View File

@ -86,6 +86,7 @@ class BootstrapTable extends PropsBaseResolver(Component) {
sortField={ store.sortField }
sortOrder={ store.sortOrder }
onSort={ this.props.onSort }
onFilter={ this.props.onFilter }
selectRow={ headerCellSelectionInfo }
/>
<Body
@ -126,7 +127,7 @@ BootstrapTable.propTypes = {
PropTypes.string
]),
pagination: PropTypes.object,
onSort: PropTypes.func,
filter: PropTypes.object,
cellEdit: PropTypes.shape({
mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired,
onUpdate: PropTypes.func,
@ -169,8 +170,10 @@ BootstrapTable.propTypes = {
dataField: PropTypes.string.isRequired,
order: PropTypes.oneOf([Const.SORT_DESC, Const.SORT_ASC]).isRequired
})),
overlay: PropTypes.func,
onTableChange: PropTypes.func,
overlay: PropTypes.func
onSort: PropTypes.func,
onFilter: PropTypes.func
};
BootstrapTable.defaultProps = {

View File

@ -6,6 +6,7 @@ import Store from './store';
import {
wrapWithCellEdit,
wrapWithSelection,
wrapWithFilter,
wrapWithSort,
wrapWithPagination
} from './table-factory';
@ -70,6 +71,8 @@ const withDataStore = Base =>
});
} else if (this.props.selectRow) {
return wrapWithSelection(baseProps);
} else if (this.props.filter) {
return wrapWithFilter(baseProps);
} else if (this.props.columns.filter(col => col.sort).length > 0) {
return wrapWithSort(baseProps);
} else if (this.props.pagination) {

View File

@ -16,12 +16,14 @@ const HeaderCell = (props) => {
onSort,
sorting,
sortOrder,
isLastSorting
isLastSorting,
onFilter
} = props;
const {
text,
sort,
filter,
hidden,
headerTitle,
headerAlign,
@ -38,10 +40,13 @@ const HeaderCell = (props) => {
..._.isFunction(headerAttrs) ? headerAttrs(column, index) : headerAttrs,
...headerEvents
};
// we are suppose to pass sortSymbol and filerElm
// the headerFormatter is not only header text but also the all of header cell customization
const children = headerFormatter ? headerFormatter(column, index) : text;
let cellStyle = {};
let sortSymbol;
let filterElm;
let cellStyle = {};
let cellClasses = _.isFunction(headerClasses) ? headerClasses(column, index) : headerClasses;
if (headerStyle) {
@ -91,12 +96,14 @@ const HeaderCell = (props) => {
}
if (cellClasses) cellAttrs.className = cs(cellAttrs.className, cellClasses);
if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle;
if (filter) {
filterElm = <filter.Filter { ...filter.props } onFilter={ onFilter } column={ column } />;
}
return (
<th { ...cellAttrs }>
{ children }{ sortSymbol }
{ children }{ sortSymbol }{ filterElm }
</th>
);
};
@ -126,13 +133,16 @@ HeaderCell.propTypes = {
editable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
editCellStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
editCellClasses: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
validator: PropTypes.func
validator: PropTypes.func,
filter: PropTypes.object,
filterValue: PropTypes.func
}).isRequired,
index: PropTypes.number.isRequired,
onSort: PropTypes.func,
sorting: PropTypes.bool,
sortOrder: PropTypes.oneOf([Const.SORT_ASC, Const.SORT_DESC]),
isLastSorting: PropTypes.bool
isLastSorting: PropTypes.bool,
onFilter: PropTypes.func
};
export default HeaderCell;

View File

@ -12,6 +12,7 @@ const Header = (props) => {
const {
columns,
onSort,
onFilter,
sortField,
sortOrder,
selectRow
@ -36,6 +37,7 @@ const Header = (props) => {
column={ column }
onSort={ onSort }
sorting={ currSort }
onFilter={ onFilter }
sortOrder={ sortOrder }
isLastSorting={ isLastSorting }
/>);
@ -49,6 +51,7 @@ const Header = (props) => {
Header.propTypes = {
columns: PropTypes.array.isRequired,
onSort: PropTypes.func,
onFilter: PropTypes.func,
sortField: PropTypes.string,
sortOrder: PropTypes.string,
selectRow: PropTypes.object

View File

@ -26,6 +26,16 @@ class SortWrapper extends Component {
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.isDataChanged) {
const sortedColumn = nextProps.columns.find(
column => column.dataField === nextProps.store.sortField);
if (sortedColumn) {
nextProps.store.sortBy(sortedColumn, nextProps.store.sortOrder);
}
}
}
handleSort(column) {
const { store } = this.props;
store.sortBy(column);

View File

@ -6,11 +6,12 @@ import { getRowByRowId } from './rows';
export default class Store {
constructor(keyField) {
this._data = [];
this._filteredData = [];
this._keyField = keyField;
this._sortOrder = undefined;
this._sortField = undefined;
this._selected = [];
this._filtering = false;
}
edit(rowId, dataField, newValue) {
@ -24,8 +25,26 @@ export default class Store {
this.data = sort(this)(sortFunc);
}
get data() { return this._data; }
set data(data) { this._data = (data ? JSON.parse(JSON.stringify(data)) : []); }
getAllData() {
return this._data;
}
get data() {
if (this._filtering) {
return this._filteredData;
}
return this._data;
}
set data(data) {
if (this._filtering) {
this._filteredData = data;
} else {
this._data = (data ? JSON.parse(JSON.stringify(data)) : []);
}
}
get filteredData() { return this._filteredData; }
set filteredData(filteredData) { this._filteredData = filteredData; }
get keyField() { return this._keyField; }
set keyField(keyField) { this._keyField = keyField; }
@ -38,4 +57,7 @@ export default class Store {
get selected() { return this._selected; }
set selected(selected) { this._selected = selected; }
get filtering() { return this._filtering; }
set filtering(filtering) { this._filtering = filtering; }
}

View File

@ -1,6 +1,7 @@
/* eslint react/prop-types: 0 */
import React from 'react';
import _ from './utils';
import BootstrapTable from './bootstrap-table';
import SortWrapper from './sort/wrapper';
import RowSelectionWrapper from './row-selection/wrapper';
@ -19,6 +20,14 @@ export const wrapWithSort = props =>
export const pureTable = props =>
React.createElement(BootstrapTable, { ...props });
export const wrapWithFilter = (props) => {
if (props.filter) {
const { FilterWrapper } = props.filter;
return React.createElement(FilterWrapper, { ...props, baseElement: wrapWithSort, _ });
}
return wrapWithSort(props);
};
export const wrapWithPagination = (props) => {
if (props.pagination) {
const { PaginationWrapper } = props.pagination;
@ -29,6 +38,6 @@ export const wrapWithPagination = (props) => {
export const sortableElement = props => wrapWithPagination(props);
export const selectionElement = props => wrapWithSort(props);
export const selectionElement = props => wrapWithFilter(props);
export const cellEditElement = props => wrapWithSelection(props);

View File

@ -1,5 +1,6 @@
import 'jsdom-global/register';
import React from 'react';
import sinon from 'sinon';
import { shallow, mount } from 'enzyme';
import Const from '../../src/const';
@ -114,4 +115,38 @@ describe('SortWrapper', () => {
expect(store.sortOrder).toEqual(defaultSorted[0].order);
});
});
describe('componentWillReceiveProps', () => {
let nextProps;
beforeEach(() => {
nextProps = { columns, store };
store.sortField = columns[1].dataField;
store.sortOrder = Const.SORT_DESC;
});
describe('if nextProps.isDataChanged is true', () => {
beforeEach(() => {
nextProps.isDataChanged = true;
store.sortBy = sinon.stub();
});
it('should sorting again', () => {
wrapper.instance().componentWillReceiveProps(nextProps);
expect(store.sortBy.calledOnce).toBeTruthy();
});
});
describe('if nextProps.isDataChanged is false', () => {
beforeEach(() => {
nextProps.isDataChanged = false;
store.sortBy = sinon.stub();
});
it('should not sorting', () => {
wrapper.instance().componentWillReceiveProps(nextProps);
expect(store.sortBy.calledOnce).toBeFalsy();
});
});
});
});