mirror of
https://github.com/gosticks/react-bootstrap-table2.git
synced 2025-10-16 11:55:39 +00:00
commit
80b1ac3370
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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'],
|
||||
|
||||
74
packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js
vendored
Normal file
74
packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js
vendored
Normal 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>
|
||||
);
|
||||
68
packages/react-bootstrap-table2-example/examples/column-filter/custom-text-filter.js
vendored
Normal file
68
packages/react-bootstrap-table2-example/examples/column-filter/custom-text-filter.js
vendored
Normal 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>
|
||||
);
|
||||
55
packages/react-bootstrap-table2-example/examples/column-filter/text-filter-default-value.js
vendored
Normal file
55
packages/react-bootstrap-table2-example/examples/column-filter/text-filter-default-value.js
vendored
Normal 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>
|
||||
);
|
||||
56
packages/react-bootstrap-table2-example/examples/column-filter/text-filter-eq-comparator.js
vendored
Normal file
56
packages/react-bootstrap-table2-example/examples/column-filter/text-filter-eq-comparator.js
vendored
Normal 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>
|
||||
);
|
||||
51
packages/react-bootstrap-table2-example/examples/column-filter/text-filter.js
vendored
Normal file
51
packages/react-bootstrap-table2-example/examples/column-filter/text-filter.js
vendored
Normal 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>
|
||||
);
|
||||
@ -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",
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 />)
|
||||
|
||||
40
packages/react-bootstrap-table2-filter/README.md
Normal file
40
packages/react-bootstrap-table2-filter/README.md
Normal 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
|
||||
});
|
||||
```
|
||||
11
packages/react-bootstrap-table2-filter/package.json
Normal file
11
packages/react-bootstrap-table2-filter/package.json
Normal 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"
|
||||
}
|
||||
2
packages/react-bootstrap-table2-filter/src/comparison.js
vendored
Normal file
2
packages/react-bootstrap-table2-filter/src/comparison.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export const LIKE = 'LIKE';
|
||||
export const EQ = '=';
|
||||
107
packages/react-bootstrap-table2-filter/src/components/text.js
vendored
Normal file
107
packages/react-bootstrap-table2-filter/src/components/text.js
vendored
Normal 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;
|
||||
5
packages/react-bootstrap-table2-filter/src/const.js
vendored
Normal file
5
packages/react-bootstrap-table2-filter/src/const.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export const FILTER_TYPE = {
|
||||
TEXT: 'TEXT'
|
||||
};
|
||||
|
||||
export const FILTER_DELAY = 500;
|
||||
45
packages/react-bootstrap-table2-filter/src/filter.js
vendored
Normal file
45
packages/react-bootstrap-table2-filter/src/filter.js
vendored
Normal 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;
|
||||
};
|
||||
15
packages/react-bootstrap-table2-filter/src/index.js
vendored
Normal file
15
packages/react-bootstrap-table2-filter/src/index.js
vendored
Normal 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
|
||||
});
|
||||
49
packages/react-bootstrap-table2-filter/src/wrapper.js
vendored
Normal file
49
packages/react-bootstrap-table2-filter/src/wrapper.js
vendored
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
packages/react-bootstrap-table2-filter/test/filter.test.js
Normal file
94
packages/react-bootstrap-table2-filter/test/filter.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
168
packages/react-bootstrap-table2-filter/test/wrapper.test.js
Normal file
168
packages/react-bootstrap-table2-filter/test/wrapper.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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));
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user