Repository: gregnb/mui-datatables
Pull request #889 (2019-08-30 21:36:08)

Feature/issue 871 (setCellHeaderProps) and Feature/issue 714 (setTableProps)
https://github.com/gregnb/mui-datatables/issues/871 and https://github.com/gregnb/mui-datatables/issues/714

Added two new options:

* setTableProps (options)
* setCellHeaderProps (columns)

They're modeled after the existing setCellProps and setRowProps. The dense table is really only available in Material v4, so the example only removes some padding in Material v3.

Quick view: https://codesandbox.io/s/github/patorjk/mui-datatables/tree/set-props

coveralls: 
[![Coverage Status](https://coveralls.io/builds/25454923/badge)](https://coveralls.io/builds/25454923)

Coverage decreased (-0.04%) to 76.177% when pulling **fe8ccf60d41bb001d9cabb7f0f33d64677eef1eb on patorjk:set-props** into **3099decd2de8661565edfff328bc7a535d1d4b2f on gregnb:master**.

patorjk: I'm glad this is on your radar. Since discovering setCellProps and setRowsProps I've found them incredibly useful. They provide a lot of power and a nice reprieve from render functions. Some examples:

* Using setCellProps I'm able to eliminate 1,700 divs from one of my tables (previously I was wrapping every cell in a div with a style of "white-space:pre").
* Using setCellProps and setCellHeaderProps I'm able to have more concise code.
* setCellProps and setCellHeaderProps make the issue discussed here: https://github.com/gregnb/mui-datatables/issues/681 - completely trivial. 

Render functions technically allow for the same thing, but I find them dreadful and they're way more complex than would seem. For example, to override a cell header, you really need to add in most of the functionality of [TableHeadCell.js](https://github.com/gregnb/mui-datatables/blob/master/src/components/TableHeadCell.js), and there's a lot going on in there. I don't think it's reasonable to assume that most devs will take the care to reproduce what's in there, leading to bugs in their code.

Anyway, some time has passed since I put this in. setTableProps may be better off as "tableProps", as there's only 1 table. Additionally, if you decided you like this type of API, you may want to reconsider the searchProps idea of https://github.com/gregnb/mui-datatables/pull/808 (though that implementation is buggy - it's very close though). searchProps also makes a lot of customization trivial, and itsolves a lot of issues people brought up in https://github.com/gregnb/mui-datatables/issues/857#issuecomment-530179241.
gabrielliwerant: Understood about props vs render, I agree that custom render functions are a bad option when you want to replicate the functionality almost entirely, but with some slight changes. I think it would be a nice option to allow various parts of the table to be exported separately for just this kind of usage, but that's probably a ways away. I think render functions work best when the functionality being added is very custom and much different from exists, and probably has a bunch of nested components and such. But use cases like those you mention are better served via adding props, so it's nice to have both.
gabrielliwerant: Hm, I don't see a big difference between `setTableProps`  and `tableProps` for the API. I think it's better to be consistent with the other options for now. Maybe there's an even better organizing principle that can be used for API naming, like how callbacks all begin with `on`. I've also been toying with the idea of adding a prefix for boolean options because as of now they are difficult to distinguish from many of the non-boolean options. So this is something I do think about.

Agreed about `searchProps` having valid use cases outside of the `customSearchRender`. I'll ping the PR creator in the next day or so and see if he wants to update it. If not, It'll be up for grabs.
patorjk: I can circle back a little later for tests.
gabrielliwerant: @patorjk If you want to take on `setSearchProps`, go for it. I'm going to assume the other dev that submitted the old PR isn't checking up on it, since it's gotten old and he hasn't responded to new messages.
patorjk: Sure, I'll put something together, I'll loop back to it later this week or next.

Patch:
diff --git a/README.md b/README.md
index cd54678..a31d7f5 100644
--- a/README.md
+++ b/README.md
@@ -195,8 +195,8 @@ The component accepts the following props:
 |**`onColumnViewChange`**|function||Callback function that triggers when a column view has been changed. `function(changedColumn: string, action: string) => void`
 |**`onTableChange`**|function||Callback function that triggers when table state has changed. `function(action: string, tableState: object) => void`
 |**`onTableInit`**|function||Callback function that triggers when table state has been initialized. `function(action: string, tableState: object) => void`
-|**`setRowProps`**|function||Is called for each row and allows to return custom props for this row based on its data. `function(row: array, dataIndex: number) => object`
-|**`setTableProps`**|function||Is called to set properties to be applied to the material-ui table component. `function() => object`
+|**`setRowProps`**|function||Is called for each row and allows you to return custom props for this row based on its data. `function(row: array, dataIndex: number) => object` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-styling/index.js)
+|**`setTableProps`**|function||Is called for the table and allows you to return custom props for the table based on its data. `function() => object` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-styling/index.js)
 
 ## Customize Columns
 
@@ -243,7 +243,8 @@ const columns = [
 |**`hint`**|string||Display hint icon with string as tooltip on hover.
 |**`customHeadRender`**|function||Function that returns a string or React component. Used as display for column header. `function(columnMeta, handleToggleColumn) => string`&#124;` React Component`
 |**`customBodyRender`**|function||Function that returns a string or React component. Used as display data within all table cells of a given column. `function(value, tableMeta, updateValue) => string`&#124;` React Component` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/component/index.js)
-|**`setCellProps`**|function||Is called for each cell and allows to return custom props for this cell based on its data. `function(cellValue: string, rowIndex: number, columnIndex: number) => object`
+|**`setCellProps`**|function||Is called for each cell and allows to you return custom props for this cell based on its data. `function(cellValue: string, rowIndex: number, columnIndex: number) => object` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-styling/index.js)
+|**`setCellHeaderProps`**|function||Is called for each header cell and allows you to return custom props for the header cell based on its data. `function(columnMeta: object) => object` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-styling/index.js)
 
 `customHeadRender` is called with these arguments:
 
diff --git a/examples/customize-styling/index.js b/examples/customize-styling/index.js
index 5166866..2ac703e 100644
--- a/examples/customize-styling/index.js
+++ b/examples/customize-styling/index.js
@@ -1,11 +1,14 @@
 import React from "react";
 import MUIDataTable from "../../src/";
 import {createMuiTheme, MuiThemeProvider, withStyles} from '@material-ui/core/styles';
+import Switch from '@material-ui/core/Switch';
+import FormGroup from '@material-ui/core/FormGroup';
+import FormControlLabel from '@material-ui/core/FormControlLabel';
 import classnames from 'classnames';
 
 const customStyles = {
   BusinessAnalystRow: {
-    '& td': {backgroundColor: "#F00"}
+    '& td': {backgroundColor: "#FAA"}
   },
   NameCell: {
     fontWeight: 900
@@ -14,11 +17,19 @@ const customStyles = {
 
 class Example extends React.Component {
 
+  constructor(props) {
+    super(props);
+    this.state = {
+      denseTable: false,
+      stacked: true
+    };
+  }
+
   getMuiTheme = () => createMuiTheme({
     overrides: {
       MUIDataTable: {
         root: {
-          backgroundColor: "#FF000",
+          backgroundColor: "#AAF",
         },
         paper: {
           boxShadow: "none",
@@ -32,6 +43,18 @@ class Example extends React.Component {
     }
   });
 
+  toggleDenseTable = (event) => {
+    this.setState({
+      denseTable: event.target.checked
+    });
+  }
+
+  toggleResponsive = (event) => {
+    this.setState({
+      stacked: event.target.checked ? true : false
+    });
+  }
+
   render() {
     const columns = [
       {
@@ -45,6 +68,17 @@ class Example extends React.Component {
                   [this.props.classes.NameCell]: value === "Mel Brooks"
                 })
             };
+          },
+          setCellHeaderProps: (value) => {
+            return {
+              className: classnames(
+                {
+                  [this.props.classes.NameCell]: true
+                }),
+                style: {
+                  textDecoration: 'underline'
+                }
+            };
           }
         }
       },
@@ -52,6 +86,7 @@ class Example extends React.Component {
         name: "Title",
         options: {
           filter: true,
+          setCellHeaderProps: (value) => ({style:{textDecoration:'underline'}}),
         }
       },
       {
@@ -111,7 +146,9 @@ class Example extends React.Component {
     const options = {
       filter: true,
       filterType: 'dropdown',
-      responsive: 'stacked',
+      responsive: this.state.stacked ? 'stacked' : 'scrollMaxHeight',
+      fixedHeader: true,
+      rowHover: false,
       setRowProps: (row) => {
         return {
           className: classnames(
@@ -120,12 +157,44 @@ class Example extends React.Component {
             }),
           style: {border: '3px solid blue',}
         };
+      },
+      setTableProps: () => {
+        return {
+          padding: this.state.denseTable ? "none" : "default",
+          
+          // material ui v4 only
+          size: this.state.denseTable ? "small" : "medium",
+        };
       }
 
     };
 
     return (
       <MuiThemeProvider theme={this.getMuiTheme()}>
+        <FormGroup row>
+          <FormControlLabel
+            control={
+              <Switch
+                checked={this.state.denseTable}
+                onChange={this.toggleDenseTable}
+                value="denseTable"
+                color="primary"
+              />
+            }
+            label="Dense Table"
+          />
+          <FormControlLabel
+            control={
+              <Switch
+                checked={this.state.stacked}
+                onChange={this.toggleResponsive}
+                value="stacked"
+                color="primary"
+              />
+            }
+            label="Stacked Table"
+          />
+        </FormGroup>
         <MUIDataTable title={"ACME Employee list"} data={data} columns={columns} options={options}/>
       </MuiThemeProvider>
     );
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index 5451042..7efd880 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -128,6 +128,8 @@ class MUIDataTable extends React.Component {
               }),
             ]),
             customFilterListRender: PropTypes.func,
+            setCellProps: PropTypes.func,
+            setCellHeaderProps: PropTypes.func,
           }),
         }),
       ]),
@@ -193,6 +195,7 @@ class MUIDataTable extends React.Component {
       }),
       onDownload: PropTypes.func,
       setTableProps: PropTypes.func,
+      setRowProps: PropTypes.func,
     }),
     /** Pass and use className to style MUIDataTable as desired */
     className: PropTypes.string,
@@ -1336,6 +1339,10 @@ class MUIDataTable extends React.Component {
         break;
     }
 
+    let tableProps = this.options.setTableProps ? this.options.setTableProps() : {};
+    let tableClassNames = classnames(classes.tableRoot, tableProps.className);
+    delete tableProps.className; // remove className from props to avoid the className being applied twice
+
     return (
       <Paper
         elevation={this.options.elevation}
@@ -1397,7 +1404,12 @@ class MUIDataTable extends React.Component {
               setResizeable={fn => (this.setHeadResizeable = fn)}
             />
           )}
-          <MuiTable ref={el => (this.tableRef = el)} tabIndex={'0'} role={'grid'} {...this.getTableProps()}>
+          <MuiTable
+            ref={el => (this.tableRef = el)}
+            tabIndex={'0'}
+            role={'grid'}
+            className={tableClassNames}
+            {...tableProps}>
             <caption className={classes.caption}>{title}</caption>
             <TableHead
               columns={columns}
diff --git a/src/components/TableBodyCell.js b/src/components/TableBodyCell.js
index dd5f584..e90f4dc 100644
--- a/src/components/TableBodyCell.js
+++ b/src/components/TableBodyCell.js
@@ -12,18 +12,26 @@ const defaultBodyCellStyles = theme => ({
     [theme.breakpoints.down('sm')]: {
       display: 'inline-block',
       fontSize: '16px',
-      height: '24px',
-      width: 'calc(50% - 80px)',
+      width: '50%',
       whiteSpace: 'nowrap',
+      boxSizing: 'border-box',
+      height: '32px',
+      '&:nth-last-child(2)': {
+        borderBottom: 'none'
+      },
     },
   },
   responsiveStacked: {
     [theme.breakpoints.down('sm')]: {
       display: 'inline-block',
       fontSize: '16px',
-      width: 'calc(50% - 80px)',
+      width: '50%',
       whiteSpace: 'nowrap',
-      height: '24px',
+      boxSizing: 'border-box',
+      height: '32px',
+      '&:last-child': {
+        borderBottom: 'none'
+      },
     },
   },
 });
diff --git a/src/components/TableHead.js b/src/components/TableHead.js
index 7ef54b7..3be2a10 100644
--- a/src/components/TableHead.js
+++ b/src/components/TableHead.js
@@ -78,6 +78,7 @@ class TableHead extends React.Component {
                 column.customHeadRender({ index, ...column }, this.handleToggleColumn)
               ) : (
                 <TableHeadCell
+                  cellHeaderProps={columns[index].setCellHeaderProps ? columns[index].setCellHeaderProps({ index, ...column }) : {}}
                   key={index}
                   index={index}
                   type={'cell'}
diff --git a/src/components/TableHeadCell.js b/src/components/TableHeadCell.js
index ea8169e..8ab66ed 100644
--- a/src/components/TableHeadCell.js
+++ b/src/components/TableHeadCell.js
@@ -91,7 +91,10 @@ class TableHeadCell extends React.Component {
 
   render() {
     const { isSortTooltipOpen, isHintTooltipOpen } = this.state;
-    const { children, classes, options, sortDirection, sort, hint, print, column } = this.props;
+
+    const { children, classes, options, sortDirection, sort, hint, print, column, cellHeaderProps = {} } = this.props;
+    const { className, ...otherProps } = cellHeaderProps;
+
     const sortActive = sortDirection !== 'none' && sortDirection !== undefined ? true : false;
     const ariaSortDirection = sortDirection === 'none' ? false : sortDirection;
 
@@ -102,14 +105,17 @@ class TableHeadCell extends React.Component {
       ...(ariaSortDirection ? { direction: sortDirection } : {}),
     };
 
-    const cellClass = classNames({
-      [classes.root]: true,
-      [classes.fixedHeader]: options.fixedHeader,
-      'datatables-noprint': !print,
-    });
+    const cellClass = classNames(
+      {
+        [classes.root]: true,
+        [classes.fixedHeader]: options.fixedHeader,
+        'datatables-noprint': !print,
+      },
+      className,
+    );
 
     return (
-      <TableCell className={cellClass} scope={'col'} sortDirection={ariaSortDirection}>
+      <TableCell className={cellClass} scope={'col'} sortDirection={ariaSortDirection} {...otherProps}>
         {options.sort && sort ? (
           <Tooltip
             title={


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index 1d22516..21127ca 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { spy } from 'sinon';
+import { spy, stub } from 'sinon';
 import { mount, shallow } from 'enzyme';
 import { assert, expect } from 'chai';
 import cloneDeep from 'lodash.clonedeep';
@@ -471,6 +471,22 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(initializeTableSpy.callCount, 1);
   });
 
+  it('should add custom props to table if setTableProps provided', () => {
+    const options = { setTableProps: stub().returns({ myProp: 'test', className: 'testClass' }) };
+    const fullWrapper = mount(<MUIDataTable columns={columns} data={[]} options={options} />);
+    const props = fullWrapper
+      .find('table')
+      .first()
+      .props();
+
+    const classNames = props.className.split(' ');
+    const finalClass = classNames[classNames.length - 1];
+
+    assert.strictEqual(props.myProp, 'test');
+    assert.strictEqual(finalClass, 'testClass');
+    assert.isAtLeast(options.setTableProps.callCount, 1);
+  });
+
   it('should correctly build internal filterList structure', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />);
     const state = shallowWrapper.dive().state();
diff --git a/test/MUIDataTableHeadCell.test.js b/test/MUIDataTableHeadCell.test.js
index d7e4c3b..73342e5 100644
--- a/test/MUIDataTableHeadCell.test.js
+++ b/test/MUIDataTableHeadCell.test.js
@@ -4,6 +4,7 @@ import { mount, shallow } from 'enzyme';
 import { assert, expect, should } from 'chai';
 import textLabels from '../src/textLabels';
 import TableHeadCell from '../src/components/TableHeadCell';
+import TableCell from '@material-ui/core/TableCell';
 import TableSortLabel from '@material-ui/core/TableSortLabel';
 import HelpIcon from '@material-ui/icons/Help';
 
@@ -16,6 +17,27 @@ describe('<TableHeadCell />', function() {
     };
   });
 
+  it('should add custom props to header cell if "setCellHeaderProps" provided', () => {
+    const options = { sort: true, textLabels };
+    const toggleSort = () => {};
+    const setCellHeaderProps = { myProp: 'test', className: 'testClass' };
+    const selectRowUpdate = stub();
+    const toggleExpandRow = () => {};
+
+    const mountWrapper = mount(
+      <TableHeadCell cellHeaderProps={setCellHeaderProps} options={options} sortDirection={'asc'} sort={true} toggleSort={toggleSort} classes={classes}>
+        some content
+      </TableHeadCell>,
+    );
+
+    const props = mountWrapper.find(TableCell).props();
+    const classNames = props.className.split(' ');
+    const finalClass = classNames[classNames.length - 1];
+
+    assert.strictEqual(props.myProp, 'test');
+    assert.strictEqual(finalClass, 'testClass');
+  });
+
   it('should render a table head cell with sort label when options.sort = true provided', () => {
     const options = { sort: true, textLabels };
     const toggleSort = () => {};