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?
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?
responsive
prop to the Table component,<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.
*/
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);
I have a preference for option 1.
I could find the following for benchmark:
Most helpful comment
As a workaround, it's relatively easy to wrap the table in a div:
YMMV based on parent hierarchy, but I've had good luck with this approach inside of a
Grid
component.