Material-ui: [Table] Responsive option

Created on 18 Nov 2017  路  6Comments  路  Source: mui-org/material-ui

I notice there is currently no option to make the table "responsive" like in Bootstrap. I think it would be a good addition since this framework is also used a lot on mobile devices.

I'd just add a responsive prop like this:

<Table responsive>

And the code behind is basically this style code applyed to the <Table> element.

const styles = theme => ({
  table: {
    display: 'block',
    width: '100%',
    overflowX: 'auto',
  },
});

What do you think?

Table enhancement good first issue

Most helpful comment

As a workaround, it's relatively easy to wrap the table in a div:

<div style={{width: 'auto', overflowX: 'scroll'}}>
  <Table>
    ...
  </Table>
</div>

YMMV based on parent hierarchy, but I've had good luck with this approach inside of a Grid component.

All 6 comments

In the approach of responsive tables, there are many options. I think that it would be steeper not to add this option without explicitly indicating how to solve the table mapping problem. And to make individual options that would indicate that the table will be rotated or will scroll.

below is my version of responsive tables with pagination and sort. I want to add a search bar to it soon.

import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TablePagination from '@material-ui/core/TablePagination';
import TableRow from '@material-ui/core/TableRow';
import TableSortLabel from '@material-ui/core/TableSortLabel';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import Checkbox from '@material-ui/core/Checkbox';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import DeleteIcon from '@material-ui/icons/Delete';
import FilterListIcon from '@material-ui/icons/FilterList';
import { lighten } from '@material-ui/core/styles/colorManipulator';
import Grid from '@material-ui/core/Grid';
import Hidden from "@material-ui/core/Hidden";


let counter = 0;
function createData(name, calories, fat, carbs, protein) {
    counter += 1;
    return { id: counter, name, calories, fat, carbs, protein };
}

const columnData = [
  { id: 'name', numeric: false, disablePadding: false, label: 'Dessert (100g serving)' },
  { id: 'calories', numeric: true, disablePadding: false, label: 'Calories' },
  { id: 'fat', numeric: true, disablePadding: false, label: 'Fat (g)' },
  { id: 'carbs', numeric: true, disablePadding: false, label: 'Carbs (g)' },
  { id: 'protein', numeric: true, disablePadding: false, label: 'Protein (g)' },
];

class EnhancedTableHead extends React.Component {
    createSortHandler = property => event => {
      this.props.onRequestSort(event, property);
    };

    render() {
        const { onSelectAllClick, order, orderBy, numSelected, rowCount } = this.props;

        return (<Hidden only="xs">
          <TableHead>

            <TableRow>
              <TableCell padding="checkbox">
            <Grid xs={12} sm={6}>
                <Checkbox
        indeterminate={numSelected > 0 && numSelected < rowCount}
        checked={numSelected === rowCount}
        onChange={onSelectAllClick}
      />
    </Grid>
    </TableCell>

        {columnData.map(column => {
            return (

              <TableCell
                key={column.id}
              numeric={column.numeric}
        padding={column.disablePadding ? 'none' : 'default'}
        sortDirection={orderBy === column.id ? order : false}
      >
        <Tooltip
        title="Sort"
        placement={column.numeric ? 'bottom-end' : 'bottom-start'}
        enterDelay={300}
      >
        <TableSortLabel
        active={orderBy === column.id}
        direction={order}
        onClick={this.createSortHandler(column.id)}
      >
            {column.label}
            </TableSortLabel>
          </Tooltip>
        </TableCell>
              );
        }, this)}
  </TableRow>

            </TableHead>
            </Hidden>

);
    }
}

EnhancedTableHead.propTypes = {
    numSelected: PropTypes.number.isRequired,
    onRequestSort: PropTypes.func.isRequired,
    onSelectAllClick: PropTypes.func.isRequired,
    order: PropTypes.string.isRequired,
    orderBy: PropTypes.string.isRequired,
    rowCount: PropTypes.number.isRequired,
};

const toolbarStyles = theme => ({
    root: {
        paddingRight: theme.spacing.unit,

    },
    highlight:
      theme.palette.type === 'light'
        ? {
            color: theme.palette.secondary.main,
            backgroundColor: lighten(theme.palette.secondary.light, 0.85),
        }
        : {
            color: theme.palette.text.primary,
            backgroundColor: theme.palette.secondary.dark,
        },
    spacer: {
        flex: '1 1 100%',
    },
    actions: {
        color: theme.palette.text.secondary,
    },
    title: {
        flex: '0 0 auto',
    },
});

let EnhancedTableToolbar = props => {
    const { numSelected, classes } = props;

    return (
      <Toolbar
        className={classNames(classes.root, {
          [classes.highlight]: numSelected > 0,
        })}
    >
      <div className={classes.title}>
        {numSelected > 0 ? (
          <Typography color="inherit" variant="subheading">
            {numSelected} selected
          </Typography>
        ) : (
          <Typography variant="title" id="tableTitle">
            AppApproval
          </Typography>
        )}
</div>
<div className={classes.spacer} />
<div className={classes.actions}>
  {numSelected > 0 ? (
    <Tooltip title="Delete">
      <IconButton aria-label="Delete">
        <DeleteIcon />
      </IconButton>
    </Tooltip>
  ) : (
    <Tooltip title="Filter list">
      <IconButton aria-label="Filter list">
        <FilterListIcon />
      </IconButton>
    </Tooltip>
        )}
</div>
</Toolbar>
  );
};

EnhancedTableToolbar.propTypes = {
    classes: PropTypes.object.isRequired,
    numSelected: PropTypes.number.isRequired,
};

EnhancedTableToolbar = withStyles(toolbarStyles)(EnhancedTableToolbar);

const styles = theme => ({
    root: {
        marginTop: theme.spacing.unit * 3
    },

    tableBodyRow: {
        //Small Screen
        display: "block",
        height: "auto",
        marginTop: 10,
        backgroundColor: "lightgrey",

        [theme.breakpoints.up("sm")]: {
            height: 48,
            display: "table-row",
            border: 0,
            backgroundColor: "#ffffff"
        }
    },

    tableBodyData: {
        display: "block",
        padding: 12,
        fontSize: 14,
        textAlign: "right",
        border: 0,

        // Adding each data table head from here
        "&:before": {
            content: "attr(datatitle)",
            float: "left",
            fontWeight: 600,
            color: "#00000"
        },

        [theme.breakpoints.up("sm")]: {
            display: "table-cell",
            padding: "20px 24px",
            fontSize: 14,
            textAlign: "left",
            borderBottom: "1px solid #ccc",

            "&:before": {
                content: "",
                display: "none"
            }
        }
    }
});

class EnhancedTable extends React.Component {
    constructor(props, context) {
        super(props, context);

        this.state = {
            order: 'asc',
            orderBy: 'calories',
            selected: [],
            data: [
              createData('Cupcake', 305, 3.7, 67, 4.3),
              createData('Donut', 452, 25.0, 51, 4.9),
              createData('Eclair', 262, 16.0, 24, 6.0),
              createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
              createData('Gingerbread', 356, 16.0, 49, 3.9),
              createData('Honeycomb', 408, 3.2, 87, 6.5),
              createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
              createData('Jelly Bean', 375, 0.0, 94, 0.0),
              createData('KitKat', 518, 26.0, 65, 7.0),
              createData('Lollipop', 392, 0.2, 98, 0.0),
              createData('Marshmallow', 318, 0, 81, 2.0),
              createData('Nougat', 360, 19.0, 9, 37.0),
              createData('Oreo', 437, 18.0, 63, 4.0),
            ].sort((a, b) => (a.calories < b.calories ? -1 : 1)),
            page: 0,
            rowsPerPage: 5,
        };
    }

    handleRequestSort = (event, property) => {
        const orderBy = property;
        let order = 'desc';

        if (this.state.orderBy === property && this.state.order === 'desc') {
            order = 'asc';
        }

        const data =
          order === 'desc'
            ? this.state.data.sort((a, b) => (b[orderBy] < a[orderBy] ? -1 : 1))
            : this.state.data.sort((a, b) => (a[orderBy] < b[orderBy] ? -1 : 1));

        this.setState({ data, order, orderBy });
    };

        handleSelectAllClick = (event, checked) => {
            if (checked) {
                this.setState({ selected: this.state.data.map(n => n.id) });
                return;
            }
            this.setState({ selected: [] });
        };

        handleClick = (event, id) => {
            const { selected } = this.state;
            const selectedIndex = selected.indexOf(id);
            let newSelected = [];

            if (selectedIndex === -1) {
                newSelected = newSelected.concat(selected, id);
            } else if (selectedIndex === 0) {
                newSelected = newSelected.concat(selected.slice(1));
            } else if (selectedIndex === selected.length - 1) {
                newSelected = newSelected.concat(selected.slice(0, -1));
            } else if (selectedIndex > 0) {
                newSelected = newSelected.concat(
                  selected.slice(0, selectedIndex),
                  selected.slice(selectedIndex + 1),
                );
            }

            this.setState({ selected: newSelected });
        };

        handleChangePage = (event, page) => {
            this.setState({ page });
        };

        handleChangeRowsPerPage = event => {
            this.setState({ rowsPerPage: event.target.value });
        };

        isSelected = id => this.state.selected.indexOf(id) !== -1;

        render() {
            const { classes } = this.props;
            const { data, order, orderBy, selected, rowsPerPage, page } = this.state;
            const emptyRows = rowsPerPage - Math.min(rowsPerPage, data.length - page * rowsPerPage);

            return (
              <Paper className={classes.root}>
                <EnhancedTableToolbar numSelected={selected.length} />

                <div className={classes.tableWrapper}>
                  <Table className={classes.table} aria-labelledby="tableTitle">
                    <EnhancedTableHead
        numSelected={selected.length}
        order={order}
        orderBy={orderBy}
        onSelectAllClick={this.handleSelectAllClick}
        onRequestSort={this.handleRequestSort}
        rowCount={data.length}
      />
      <TableBody>
        {data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(n => {
            const isSelected = this.isSelected(n.id);
            return (
              <TableRow
                hover
                onClick={event => this.handleClick(event, n.id)}
              role="checkbox"
              aria-checked={isSelected}
              tabIndex={-1}
    key={n.id}
    selected={isSelected}
    classes={{
        root: classes.tableBodyRow
    }}
  >
    <TableCell padding="checkbox"

    >
      <Checkbox checked={isSelected} />

    </TableCell>
    <TableCell
                                        datatitle="Dessert (100g serving)"
                                        classes={{ root: classes.tableBodyData }}
                                    >
    {n.name}
                                    </TableCell>
                                    <TableCell
                                        datatitle="Calories"
                                        classes={{ root: classes.tableBodyData }}
                                        numeric
                                    >
    {n.calories}
                                    </TableCell>
                                    <TableCell
                                        datatitle="Fat (g)"
                                        classes={{ root: classes.tableBodyData }}
                                        numeric
                                    >
    {n.fat}
                                    </TableCell>
                                    <TableCell
                                        datatitle="Carbs (g)"
                                        classes={{ root: classes.tableBodyData }}
                                        numeric
                                    >
    {n.carbs}
                                    </TableCell>
                                    <TableCell
                                        datatitle="Protein (g)"
                                        classes={{ root: classes.tableBodyData }}
                                        numeric
                                    >
    {n.protein}
                                    </TableCell>
  </TableRow>
);
    })}

  </TableBody>
        </Table>
      </div>


      <TablePagination
        component="div"
        count={data.length}
        rowsPerPage={rowsPerPage}
        page={page}
        backIconButtonProps={{
          'aria-label': 'Previous Page',
        }}
    nextIconButtonProps={{
      'aria-label': 'Next Page',
    }}
    onChangePage={this.handleChangePage}
    onChangeRowsPerPage={this.handleChangeRowsPerPage}
  />
</Paper>
);
    }
}

EnhancedTable.propTypes = {
    classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(EnhancedTable);

any progress or another package to handle this?

As a workaround, it's relatively easy to wrap the table in a div:

<div style={{width: 'auto', overflowX: 'scroll'}}>
  <Table>
    ...
  </Table>
</div>

YMMV based on parent hierarchy, but I've had good luck with this approach inside of a Grid component.

+1 :+1:

I can think of 2 solutions to this problem, what do you guys prefer?

  1. We add a responsive prop to the Table component,
    That can be used like this:
<Table responsive>
--- a/packages/material-ui/src/Table/Table.js
+++ b/packages/material-ui/src/Table/Table.js
@@ -23,6 +23,10 @@ export const styles = theme => ({
   stickyHeader: {
     borderCollapse: 'separate',
   },
+  /* Styles applied to the wrapper element if `responsive={true}`. */
+  responsive: {
+    overflowX: 'auto',
+  },
 });

 const Table = React.forwardRef(function Table(props, ref) {
@@ -31,6 +35,7 @@ const Table = React.forwardRef(function Table(props, ref) {
     className,
     component: Component = 'table',
     padding = 'default',
+    responsive = false,
     size = 'medium',
     stickyHeader = false,
     ...other
@@ -41,13 +46,17 @@ const Table = React.forwardRef(function Table(props, ref) {
     stickyHeader,
   ]);

+  const rootElement = (
+    <Component
+      ref={ref}
+      className={clsx(classes.root, { [classes.stickyHeader]: stickyHeader }, className)}
+      {...other}
+    />
+  );
+
   return (
     <TableContext.Provider value={table}>
-      <Component
-        ref={ref}
-        className={clsx(classes.root, { [classes.stickyHeader]: stickyHeader }, className)}
-        {...other}
-      />
+      {responsive ? <div className={classes.responsive}>{rootElement}</div> : rootElement}
     </TableContext.Provider>
   );
 });
@@ -75,6 +84,10 @@ Table.propTypes = {
    * Allows TableCells to inherit padding of the Table.
    */
   padding: PropTypes.oneOf(['default', 'checkbox', 'none']),
+  /**
+   * Allows TableCells to inherit padding of the Table.
+   */
+  responsive: PropTypes.boolean,
   /**
    * Allows TableCells to inherit size of the Table.
    */
  1. We add a new TableResponsive component that can be used like this:
<TableResponsive>
  <Table>
    {...}
  </Table>
</TableResponsive>
import React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import withStyles from '../styles/withStyles';

export const styles = {
  /* Styles applied to the root element. */
  root: {
    overflowX: 'auto',
  },
};

const TableResponsive = React.forwardRef(function TableResponsive(props, ref) {
  const { classes, className, ...other } = props;

  return <div className={clsx(classes.root, className)} ref={ref} {...other} />;
});

TableResponsive.propTypes = {
  /**
   * The content of the component.
   */
  children: PropTypes.node,
  /**
   * Override or extend the styles applied to the component.
   * See [CSS API](#css) below for more details.
   */
  classes: PropTypes.object.isRequired,
  /**
   * @ignore
   */
  className: PropTypes.string,
};

export default withStyles(styles, { name: 'MuiTableResponsive' })(TableResponsive);
  1. Any other idea?

I have a preference for option 1.

I could find the following for benchmark:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

anthony-dandrea picture anthony-dandrea  路  3Comments

newoga picture newoga  路  3Comments

FranBran picture FranBran  路  3Comments

zabojad picture zabojad  路  3Comments

finaiized picture finaiized  路  3Comments