diff --git a/docs/columns.md b/docs/columns.md index 38594d5..6546f81 100644 --- a/docs/columns.md +++ b/docs/columns.md @@ -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 ``` ## column.headerEvents - [Object] -`headerEvents` same as [`column.events`](#events) but this is for header column. \ No newline at end of file +`headerEvents` same as [`column.events`](#events) but this is for header column. + +## column.attrs - [Object | Function] +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' } +} +``` + +## column.headerAttrs - [Object | Function] +`headerAttrs` is similiar to [`column.attrs`](#attrs) but it's for header column. + +```js +{ + // omit... + headerAttrs: (column, colIndex) => ({ + // return customized HTML attribute here + }) +} +``` diff --git a/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js b/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js new file mode 100644 index 0000000..bb25ad5 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/columns/column-attrs-table.js @@ -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' +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/header-columns/column-attrs-table.js b/packages/react-bootstrap-table2-example/examples/header-columns/column-attrs-table.js new file mode 100644 index 0000000..ad2c5d9 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/header-columns/column-attrs-table.js @@ -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' +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index d63f949..16c3bae 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -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', () => ) .add('Column Event', () => ) .add('Customize Column Class', () => ) - .add('Customize Column Style', () => ); + .add('Customize Column Style', () => ) + .add('Customize Column HTML attribute', () => ); storiesOf('Work on Header Columns', module) .add('Column Formatter', () => ) @@ -64,4 +67,5 @@ storiesOf('Work on Header Columns', module) .add('Column Title', () => ) .add('Column Event', () => ) .add('Customize Column Class', () => ) - .add('Customize Column Style', () => ); + .add('Customize Column Style', () => ) + .add('Customize Column HTML attribute', () => ); diff --git a/packages/react-bootstrap-table2/src/cell.js b/packages/react-bootstrap-table2/src/cell.js index 8f590f5..4cb1621 100644 --- a/packages/react-bootstrap-table2/src/cell.js +++ b/packages/react-bootstrap-table2/src/cell.js @@ -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 ( - { content } + { content } ); }; diff --git a/packages/react-bootstrap-table2/src/header-cell.js b/packages/react-bootstrap-table2/src/header-cell.js index 062e4f6..be2e525 100644 --- a/packages/react-bootstrap-table2/src/header-cell.js +++ b/packages/react-bootstrap-table2/src/header-cell.js @@ -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 ( - + { children } ); diff --git a/packages/react-bootstrap-table2/src/utils.js b/packages/react-bootstrap-table2/src/utils.js index bee745a..e86be0f 100644 --- a/packages/react-bootstrap-table2/src/utils.js +++ b/packages/react-bootstrap-table2/src/utils.js @@ -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 }; diff --git a/packages/react-bootstrap-table2/test/cell.test.js b/packages/react-bootstrap-table2/test/cell.test.js index aebc348..1d1bebb 100644 --- a/packages/react-bootstrap-table2/test/cell.test.js +++ b/packages/react-bootstrap-table2/test/cell.test.js @@ -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( + ); + + 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( + ); + + 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( + ); + + 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( + ); + + 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( + ); + + 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( + ); + + 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( + ); + }); + + 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); + }); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/header-cell.test.js b/packages/react-bootstrap-table2/test/header-cell.test.js index 0dd027a..134de2d 100644 --- a/packages/react-bootstrap-table2/test/header-cell.test.js +++ b/packages/react-bootstrap-table2/test/header-cell.test.js @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + }); + + 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); + }); + }); + }); }); }); diff --git a/packages/react-bootstrap-table2/test/utils.test.js b/packages/react-bootstrap-table2/test/utils.test.js index feb33e9..491cc04 100644 --- a/packages/react-bootstrap-table2/test/utils.test.js +++ b/packages/react-bootstrap-table2/test/utils.test.js @@ -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); + }); + }); + }); });