* allow user to customize attrs for column cell

* [test] add test for customized cell attrs

* allow user to customize attrs for header column cell

* [test] add test for customized header cell attrs

* update document

* add new utils isObject and isEmptyObject

* make cell attrs lowest priority

* if style, event, classes, hidden, style was not given, display the
* HTML attributes if it's legal. However, if the attributes mentioned
* above was defined at the same time, column.attrs has lowest priority
* and it will be overwrited.

* [test] unit test for column.attrs

* [test] unit test for column.headerAttrs

* update Document

* rename story name

* fix lint error
This commit is contained in:
ChunMing, Chen
2017-09-12 10:47:27 -05:00
committed by Allen
parent 9199484ab6
commit 79fb2523d4
10 changed files with 469 additions and 22 deletions
+39 -1
View File
@@ -13,11 +13,13 @@ Available properties in a column object:
* [title](#title)
* [events](#events)
* [align](#align)
* [attrs](#attrs)
* [headerTitle](#headerTitle)
* [headerEvents](#headerEvents)
* [headerAlign](#headerAlign)
* [headerClasses](#headerClasses)
* [headerStyle](#headerStyle)
* [headerAttrs](#headerAttrs)
Following is a most simplest and basic usage:
@@ -189,4 +191,40 @@ You can assign any [HTML Event](https://www.w3schools.com/tags/ref_eventattribut
```
## <a name='headerEvents'>column.headerEvents - [Object]</a>
`headerEvents` same as [`column.events`](#events) but this is for header column.
`headerEvents` same as [`column.events`](#events) but this is for header column.
## <a name='attrs'>column.attrs - [Object | Function]</a>
Via `attrs` property, You can costomize table column [HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes) which allow user to configure the elements or adjust their behavior. It takes `Object` and `callback function` is also acceptable.
```js
{
// omit...
attrs: (cell, row, colIndex) => ({
// return customized HTML attribute here
})
}
```
#### * Caution
If `column.classes`, `column.style`, `column.title`, `column.hidden` or `column.align` was given at the same time, property `attrs` has lower priorty and it will be overwrited.
```js
{
// omit...
title: true, // it will be chosen.
attrs: { title: 'test' }
}
```
## <a name='headerAttrs'>column.headerAttrs - [Object | Function]</a>
`headerAttrs` is similiar to [`column.attrs`](#attrs) but it's for header column.
```js
{
// omit...
headerAttrs: (column, colIndex) => ({
// return customized HTML attribute here
})
}
```
@@ -0,0 +1,45 @@
/* eslint no-unused-vars: 0 */
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID',
attrs: { title: 'id column' }
}, {
dataField: 'name',
text: 'Product Name',
attrs: (cell, row, colIndex) => ({ 'data-test': `customized data ${colIndex}` })
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID',
attrs: { title: 'id column' }
}, {
dataField: 'name',
text: 'Product Name',
attrs: (cell, row, colIndex) => ({ 'data-test': \`customized data \${colIndex}\` })
}, {
dataField: 'price',
text: 'Product Price'
}];
<BootstrapTableful keyField='id' data={ products } columns={ columns } />
`;
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } />
<Code>{ sourceCode }</Code>
</div>
);
@@ -0,0 +1,45 @@
/* eslint no-unused-vars: 0 */
import React from 'react';
import { BootstrapTableful } from 'react-bootstrap-table2';
import Code from 'components/common/code-block';
import { productsGenerator } from 'utils/common';
const products = productsGenerator();
const columns = [{
dataField: 'id',
text: 'Product ID',
headerAttrs: { title: 'ID header column' }
}, {
dataField: 'name',
text: 'Product Name',
headerAttrs: (column, colIndex) => ({ 'data-test': `customized data ${colIndex}` })
}, {
dataField: 'price',
text: 'Product Price'
}];
const sourceCode = `\
const columns = [{
dataField: 'id',
text: 'Product ID',
headerAttrs: { title: 'ID header column' }
}, {
dataField: 'name',
text: 'Product Name',
headerAttrs: (column, colIndex) => ({ 'data-test': \`customized data \${colIndex}\` })
}, {
dataField: 'price',
text: 'Product Price'
}];
<BootstrapTableful keyField='id' data={ products } columns={ columns } />
`;
export default () => (
<div>
<BootstrapTableful keyField="id" data={ products } columns={ columns } />
<Code>{ sourceCode }</Code>
</div>
);
+6 -2
View File
@@ -20,6 +20,7 @@ import ColumnAlignTable from 'examples/columns/column-align-table';
import ColumnTitleTable from 'examples/columns/column-title-table';
import ColumnEventTable from 'examples/columns/column-event-table';
import ColumnHiddenTable from 'examples/columns/column-hidden-table';
import ColumnAttrsTable from 'examples/columns/column-attrs-table';
// work on header columns
import HeaderColumnFormatTable from 'examples/header-columns/column-format-table';
@@ -28,6 +29,7 @@ import HeaderColumnTitleTable from 'examples/header-columns/column-title-table';
import HeaderColumnEventTable from 'examples/header-columns/column-event-table';
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';
// css style
import 'bootstrap/dist/css/bootstrap.min.css';
@@ -56,7 +58,8 @@ storiesOf('Work on Columns', module)
.add('Column Hidden', () => <ColumnHiddenTable />)
.add('Column Event', () => <ColumnEventTable />)
.add('Customize Column Class', () => <ColumnClassTable />)
.add('Customize Column Style', () => <ColumnStyleTable />);
.add('Customize Column Style', () => <ColumnStyleTable />)
.add('Customize Column HTML attribute', () => <ColumnAttrsTable />);
storiesOf('Work on Header Columns', module)
.add('Column Formatter', () => <HeaderColumnFormatTable />)
@@ -64,4 +67,5 @@ storiesOf('Work on Header Columns', module)
.add('Column Title', () => <HeaderColumnTitleTable />)
.add('Column Event', () => <HeaderColumnEventTable />)
.add('Customize Column Class', () => <HeaderColumnClassTable />)
.add('Customize Column Style', () => <HeaderColumnStyleTable />);
.add('Customize Column Style', () => <HeaderColumnStyleTable />)
.add('Customize Column HTML attribute', () => <HeaderColumnAttrsTable />);
+12 -8
View File
@@ -13,15 +13,18 @@ const Cell = ({ row, rowIndex, column, columnIndex }) => {
classes,
title,
events,
align
align,
attrs
} = column;
let cellTitle;
let cellStyle = {};
let content = _.get(row, dataField);
const attrs = {
const cellAttrs = {
..._.isFunction(attrs) ? attrs(content, row, columnIndex) : attrs,
...events
};
const cellClasses = _.isFunction(classes) ? classes(content, row, columnIndex) : classes;
if (style) {
@@ -30,7 +33,7 @@ const Cell = ({ row, rowIndex, column, columnIndex }) => {
if (title) {
cellTitle = _.isFunction(title) ? title(content, row, columnIndex) : content;
attrs.title = cellTitle;
cellAttrs.title = cellTitle;
}
if (formatter) {
@@ -41,15 +44,16 @@ const Cell = ({ row, rowIndex, column, columnIndex }) => {
cellStyle.textAlign = _.isFunction(align) ? align(content, row, columnIndex) : align;
}
attrs.style = cellStyle;
attrs.className = cellClasses;
if (hidden) {
attrs.style.display = 'none';
cellStyle.display = 'none';
}
if (cellClasses) cellAttrs.className = cellClasses;
if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle;
return (
<td { ...attrs }>{ content }</td>
<td { ...cellAttrs }>{ content }</td>
);
};
+12 -8
View File
@@ -13,9 +13,12 @@ const HeaderCell = ({ column, index }) => {
headerFormatter,
headerEvents,
headerClasses,
headerStyle
headerStyle,
headerAttrs
} = column;
const attrs = {
const cellAttrs = {
..._.isFunction(headerAttrs) ? headerAttrs(column, index) : headerAttrs,
...headerEvents
};
const children = headerFormatter ? headerFormatter(column, index) : text;
@@ -28,22 +31,23 @@ const HeaderCell = ({ column, index }) => {
}
if (headerTitle) {
attrs.title = _.isFunction(headerTitle) ? headerTitle(column, index) : text;
cellAttrs.title = _.isFunction(headerTitle) ? headerTitle(column, index) : text;
}
if (headerAlign) {
cellStyle.textAlign = _.isFunction(headerAlign) ? headerAlign(column, index) : headerAlign;
}
attrs.style = cellStyle;
attrs.className = cellClasses;
if (hidden) {
attrs.style.display = 'none';
cellStyle.display = 'none';
}
if (cellClasses) cellAttrs.className = cellClasses;
if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle;
return (
<th { ...attrs }>
<th { ...cellAttrs }>
{ children }
</th>
);
+26 -1
View File
@@ -17,7 +17,32 @@ function isFunction(obj) {
return obj && (typeof obj === 'function');
}
/**
* Checks if `value` is the Object. the `Object` except `Function` and `Array.`
*
* @param {*} obj - The value gonna check
*/
function isObject(obj) {
const type = typeof obj;
return obj !== null && type === 'object' && obj.constructor === Object;
}
function isEmptyObject(obj) {
if (!isObject(obj)) return false;
const hasOwnProperty = Object.prototype.hasOwnProperty;
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i += 1) {
if (hasOwnProperty.call(obj, keys[i])) return false;
}
return true;
}
export default {
get,
isFunction
isFunction,
isObject,
isEmptyObject
};
@@ -305,4 +305,130 @@ describe('Cell', () => {
});
});
});
describe('when column.attrs prop is defined', () => {
let column;
const columnIndex = 1;
beforeEach(() => {
column = {
dataField: 'id',
text: 'ID'
};
});
describe('when attrs is an object', () => {
it('should render column.attrs correctly', () => {
column.attrs = {
'data-test': 'test',
title: 'title',
className: 'attrs-class',
style: {
backgroundColor: 'attrs-style-test',
display: 'none',
textAlign: 'right'
}
};
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
expect(wrapper.length).toBe(1);
expect(wrapper.find('td').prop('data-test')).toEqual(column.attrs['data-test']);
expect(wrapper.find('td').prop('title')).toEqual(column.attrs.title);
expect(wrapper.hasClass(column.attrs.className)).toBe(true);
expect(wrapper.find('td').prop('style')).toEqual(column.attrs.style);
expect(wrapper.find('td').prop('style').textAlign).toEqual(column.attrs.style.textAlign);
});
describe('when column.title prop is defined', () => {
it('attrs.title should be overwrited', () => {
column.title = true;
column.attrs = { title: 'title' };
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
expect(wrapper.find('td').prop('title')).toEqual(row[column.dataField]);
});
});
describe('when column.classes prop is defined', () => {
it('attrs.class should be overwrited', () => {
column.classes = 'td-test-class';
column.attrs = { className: 'attrs-class' };
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
expect(wrapper.hasClass(column.classes)).toBe(true);
});
});
describe('when column.style prop is defined', () => {
it('attrs.style should be overwrited', () => {
column.style = { backgroundColor: 'red' };
column.attrs = { style: { backgroundColor: 'attrs-style-test' } };
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
expect(wrapper.find('td').prop('style')).toEqual(column.style);
});
});
describe('when column.hidden prop is defined', () => {
it('attrs.style.hidden should be overwrited', () => {
column.hidden = true;
column.attrs = { style: { hidden: true } };
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
const style = wrapper.find('td').prop('style');
expect(style).toBeDefined();
expect(style.display).toEqual('none');
});
});
describe('when column.align prop is defined', () => {
it('attrs.style.textAlign should be overwrited', () => {
column.align = 'center';
column.attrs = { style: { textAlign: 'right' } };
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
expect(wrapper.find('td').prop('style').textAlign).toEqual(column.align);
});
});
});
describe('when attrs is custom function', () => {
let attrsCallBack;
const customAttrs = {
title: 'title',
'data-test': 'test'
};
beforeEach(() => {
attrsCallBack = sinon.stub()
.withArgs(row[column.dataField], row, columnIndex)
.returns(customAttrs);
column.attrs = attrsCallBack;
wrapper = shallow(
<Cell row={ row } columnIndex={ columnIndex } rowIndex={ 1 } column={ column } />);
});
it('should render style.attrs correctly', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('td').prop('data-test')).toEqual(customAttrs['data-test']);
expect(wrapper.find('td').prop('title')).toEqual(customAttrs.title);
});
it('should call custom attrs function correctly', () => {
expect(attrsCallBack.callCount).toBe(1);
expect(attrsCallBack.calledWith(row[column.dataField], row, columnIndex)).toBe(true);
});
});
});
});
@@ -24,9 +24,9 @@ describe('HeaderCell', () => {
expect(wrapper.text()).toEqual(column.text);
});
it('should have correct default style', () => {
it('should not have default style', () => {
const style = wrapper.find('th').prop('style');
expect(style).toBeDefined();
expect(style).toBeUndefined();
});
});
@@ -286,5 +286,103 @@ describe('HeaderCell', () => {
expect(classesCallBack.calledWith(column, index)).toBe(true);
});
});
describe('when column.headerAttrs prop is defined', () => {
beforeEach(() => {
column = {
dataField: 'id',
text: 'ID'
};
});
describe('when headerAttrs is an object', () => {
it('should render column.headerAttrs correctly', () => {
column.headerAttrs = {
'data-test': 'test',
title: 'title',
className: 'attrs-class',
style: { backgroundColor: 'attrs-style-test' }
};
wrapper = shallow(<HeaderCell column={ column } index={ index } />);
expect(wrapper.length).toBe(1);
expect(wrapper.find('th').prop('data-test')).toEqual(column.headerAttrs['data-test']);
expect(wrapper.find('th').prop('title')).toEqual(column.headerAttrs.title);
expect(wrapper.hasClass(column.headerAttrs.className)).toBe(true);
expect(wrapper.find('th').prop('style')).toEqual(column.headerAttrs.style);
});
describe('when column.headerTitle prop is defined', () => {
it('title should be overwrited', () => {
column.headerAttrs = { title: 'title' };
column.headerTitle = true;
wrapper = shallow(<HeaderCell column={ column } index={ index } />);
expect(wrapper.find('th').prop('title')).toBe(column.text);
});
});
describe('when column.headerClasses prop is defined', () => {
it('class should be overwrited', () => {
column.headerClasses = 'td-test-class';
column.headerAttrs = { className: 'attrs-class' };
wrapper = shallow(<HeaderCell column={ column } index={ index } />);
expect(wrapper.hasClass(column.headerClasses)).toBe(true);
});
});
describe('when column.headerStyle prop is defined', () => {
it('style should be overwrited', () => {
column.headerStyle = { backgroundColor: 'red' };
column.headerAttrs = { style: { backgroundColor: 'attrs-style-test' } };
wrapper = shallow(<HeaderCell column={ column } index={ index } />);
expect(wrapper.find('th').prop('style')).toEqual(column.headerStyle);
});
});
describe('when column.headerAlign prop is defined', () => {
it('style.textAlign should be overwrited', () => {
column.headerAlign = 'center';
column.headerAttrs = { style: { textAlign: 'right' } };
wrapper = shallow(<HeaderCell column={ column } index={ index } />);
expect(wrapper.find('th').prop('style').textAlign).toEqual(column.headerAlign);
});
});
});
describe('when headerAttrs is custom function', () => {
let headerAttrsCallBack;
const customHeaderAttrs = {
title: 'title',
'data-test': 'test'
};
beforeEach(() => {
headerAttrsCallBack = sinon.stub()
.withArgs(column, index)
.returns(customHeaderAttrs);
column.headerAttrs = headerAttrsCallBack;
wrapper = shallow(<HeaderCell column={ column } index={ index } />);
});
it('should render style.headerAttrs correctly', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find('th').prop('data-test')).toEqual(customHeaderAttrs['data-test']);
expect(wrapper.find('th').prop('title')).toEqual(customHeaderAttrs.title);
});
it('should call custom headerAttrs function correctly', () => {
expect(headerAttrsCallBack.callCount).toBe(1);
expect(headerAttrsCallBack.calledWith(column, index)).toBe(true);
});
});
});
});
});
@@ -21,4 +21,62 @@ describe('Utils', () => {
expect(_.get(data, 'address.not.exist')).toEqual(undefined);
});
});
describe('isObject', () => {
describe('when given Object', () => {
it('should return true', () => {
expect(_.isObject({})).toBe(true);
});
});
describe('when given Function', () => {
it('should return false', () => {
expect(_.isObject(() => 'test')).toBe(false);
});
});
describe('when given Array', () => {
it('should return false', () => {
expect(_.isObject([])).toBe(false);
});
});
describe('when given null', () => {
it('should return false', () => {
expect(_.isObject(null)).toBe(false);
});
});
});
describe('isEmptyObject', () => {
describe('when given empty Object', () => {
it('should return true', () => {
expect(_.isEmptyObject({})).toBe(true);
});
});
describe('when given non-empty Object', () => {
it('should return false', () => {
expect(_.isEmptyObject({ foo: 'test' })).toBe(false);
});
});
describe('when given Function', () => {
it('should return false', () => {
expect(_.isEmptyObject(() => 'test')).toBe(false);
});
});
describe('when given Array', () => {
it('should return false', () => {
expect(_.isEmptyObject([])).toBe(false);
});
});
describe('when given null', () => {
it('should return false', () => {
expect(_.isEmptyObject(null)).toBe(false);
});
});
});
});