An extended React Material UI Table that aims to make creating complex tables more straightforward.
npm install --save @jeremyling/react-material-ui-enhanced-table
The following packages are peer dependencies that must be installed for this package to work.
@mui/material
@mui/icons-material
@mui/lab
date-fns
lodash
Example of a collapsible table with a nested table as the collapse content.
import React, { useEffect, useState } from "react";
import EnhancedTable from "@jeremyling/react-material-ui-enhanced-table";
import { amber, green, indigo, lightGreen, red } from "@mui/material/colors";
import { Block, MoveToInbox } from "@mui/icons-material";
export default function Orders(props) {
const [data, setData] = useState([]);
const [totalCount, setTotalCount] = useState();
const [tableLoading, setTableLoading] = useState(false);
const [order, setOrder] = useState("");
const [orderBy, setOrderBy] = useState("");
const [page, setPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [selectedRow, setSelectedRow] = useState();
const [selectedItems, setSelectedItems] = useState({});
const [openRows, setOpenRows] = useState({});
const [filters, setFilters] = useState();
const [notificationsCount, setNotificationsCount] = useState();
const headers = [
{
key: "collapse",
collapse: true,
},
{ attribute: "order_number", label: "Order No" },
{
key: "status",
attribute: "status",
label: "Status",
chip: true,
chipColor: {
"In Progress": [amber[50], amber[200]],
Completed: [green[50], green[200]],
},
},
{
attribute: "updated_at",
datetime: true,
label: "Updated",
},
{
multiField: true,
key: "customer",
multiFieldData: [
{
attribute: "user.name",
},
{
attribute: "user.email",
},
{
attribute: "user.phone",
},
],
html: `
<div><strong>{{0}}</strong></div>
<div style="color: #777">{{1}}</div>
<div style="color: #777">{{2}}</div>
`,
label: "Customer",
},
{
attribute: "total",
label: "Total",
numeric: true,
price: true,
},
{
key: "actions",
actions: [
{
id: "process",
icon: <MoveToInbox />,
tooltip: "Process",
onClick: (event, row) => handleAction(event, "process", row),
color: amber[400],
hideCondition: (row) => {},
},
{
id: "cancel",
icon: <Block />,
tooltip: "Cancel",
onClick: (event, row) => handleAction(event, "cancel", row),
color: indigo[400],
},
],
label: "Actions",
},
];
const collapseContent = () => {
const collapseHeaders = [
{
key: "checkbox",
checkbox: true,
},
{
key: "image",
label: "Image",
component: (data) => (
<img src={data.image.url} alt={data.name} loading="lazy" />
),
stopPropagation: true,
},
{
multiField: true,
key: "product",
multiFieldData: [
{
attribute: "product.name",
},
{
attribute: "product.sku",
},
],
html: `
<div><strong>{{0}}</strong></div>
<div><strong style="color: #777">SKU: {{1}}</strong></div>
`,
label: "Product",
minWidth: "200px",
},
{
key: "status",
attribute: "status",
label: "Status",
chip: true,
chipColor: {
Pending: [amber[50], amber[200]],
Paid: [lightGreen[50], lightGreen[200]],
Processed: [indigo[50], indigo[200]],
Completed: [green[50], green[200]],
},
},
{
attribute: "updated_at",
datetime: true,
label: "Updated",
},
{ attribute: "quantity", label: "Qty", numeric: true },
{
key: "actions",
actions: [
{
id: "process",
icon: <MoveToInbox />,
tooltip: "Process",
onClick: (event, row) => handleAction(event, "process", row),
color: amber[400],
hideCondition: (data) => {},
},
{
id: "cancel-order",
icon: <Block />,
tooltip: "Cancel",
onClick: (event, row) => handleAction(event, "cancel", row),
color: indigo[400],
},
],
label: "Actions",
},
];
if (data) {
return data.map((order) => (
<EnhancedTable
key={order.order_number}
rows={order.order_items}
headers={collapseHeaders}
handleActionClick={(event, key) => handleAction(event, key)}
handleRowClick={(event, row) =>
handleCollapsibleTableRowClick(event, row)
}
selected={selectedItems[order.id]}
showToolbar={false}
selectibleRows={getSelectibleItems(selectedRow)}
/>
));
}
};
const handleRowClick = (event, item) => {
setSelectedRow(item);
};
function handleCollapsibleTableRowClick(event, row) {}
function getSelectibleItems(row) {}
function updateOpenRows(event, key, value) {
event.stopPropagation();
let updated = JSON.parse(JSON.stringify(openRows));
updated[key] = value;
setOpenRows(updated);
}
const handleRequestSort = (event, property) => {
if (orderBy !== property) {
setOrder("asc");
setOrderBy(property);
return;
}
switch (order) {
case "asc":
setOrder("desc");
break;
case "desc":
setOrder("");
setOrderBy("");
break;
default:
break;
}
};
const handlePageChange = (event, newPage) => {
setPage(newPage);
};
const handleRowsPerPageChange = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(1);
};
const handleDateChange = (type, value) => {
// Update filters
};
function handleAction(event, key, row = null) {}
useEffect(() => {
getData();
function getData() {
// Fetch data with filters
}
}, [order, orderBy, rowsPerPage, page, filters]);
return (
<EnhancedTable
rows={data || []}
totalCount={totalCount}
headers={headers}
order={order}
orderBy={orderBy}
loading={tableLoading}
page={page}
rowsPerPage={rowsPerPage}
handleRowClick={handleRowClick}
handleRequestSort={handleRequestSort}
handlePageChange={handlePageChange}
handleRowsPerPageChange={handleRowsPerPageChange}
handleActionClick={(event, key) => handleAction(event, key)}
handleDateChange={handleDateChange}
dates={{ from: filters.dateFrom, to: filters.dateTo }}
actionButtons={["dateFilters", "refresh"]}
collapsible={true}
collapseContent={collapseContent}
refreshBadgeCount={notificationsCount}
disableSelection={true}
openRows={openRows}
handleCollapseIconClick={updateOpenRows}
/>
);
}
Example with a nested table within each row
import React, { useEffect, useState } from "react";
import EnhancedTable from "@jeremyling/react-material-ui-enhanced-table";
import { green, red } from "@mui/material/colors";
export default function Products(props) {
const [data, setData] = useState([]);
const [totalCount, setTotalCount] = useState();
const [tableLoading, setTableLoading] = useState(true);
const [order, setOrder] = useState("asc");
const [orderBy, setOrderBy] = useState("");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [selected, setSelected] = useState({});
const [universalFilter, setUniversalFilter] = useState("");
const [nestedRowAction, setNestedRowAction] = useState({});
const [filters, setFilters] = useState();
const headers = [
{
key: "images",
label: "Images",
component: (data) => (
<img src={data.image.url} alt={data.name} loading="lazy" />
),
stopPropagation: true,
},
{
multiField: true,
key: "product",
multiFieldData: [
{
attribute: "name",
},
{
attribute: "sku",
},
],
html: `
<div><strong>{{0}}</strong></div>
<div><strong style="color: #777">SKU: {{2}}</strong></div>
`,
label: "Product",
minWidth: "200px",
},
{
key: "status",
attribute: "status",
label: "Status",
chip: true,
chipColor: {
Inactive: [red[50], red[200]],
Active: [green[50], green[200]],
},
},
{ attribute: "category", label: "Category" },
{ attribute: "stock", label: "Stock", sortable: true, numeric: true },
{
key: "price",
label: "Price",
arrayAttribute: "prices",
childAttribute: "amount",
childAttribute2: "currency",
childLabelAttribute: "tag",
childActions: { delete: true, edit: true, add: true },
numeric: true,
price: true,
},
];
const handleRowClick = (event, item) => {
if (item.id === selected.id) {
setSelected({});
return;
}
setSelected(item);
};
const handleAction = (event, key) => {};
const handleRequestSort = (event, property) => {
if (orderBy !== property) {
setOrder("asc");
setOrderBy(property);
return;
}
switch (order) {
case "asc":
setOrder("desc");
break;
case "desc":
setOrder("asc");
setOrderBy("");
break;
default:
break;
}
};
const handleUniversalFilterChange = (event) => {
setUniversalFilter(event.target.value);
};
const handlePageChange = (event, newPage) => {
setPage(newPage);
};
const handleRowsPerPageChange = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleNestedAction = (event, type, item, id) => {};
const handleNestedFieldChange = (parentId, id, attribute, value) => {};
useEffect(() => {
const getData = () => {
// Fetch data with filters
};
getData();
}, [universalFilter, order, orderBy, rowsPerPage, page, filters]);
return (
<EnhancedTable
rows={data || []}
totalCount={totalCount}
descriptorAttribute="name"
headers={headers}
order={order}
orderBy={orderBy}
loading={tableLoading}
page={page}
rowsPerPage={rowsPerPage}
selected={[selected]}
handleRowClick={handleRowClick}
handleRequestSort={handleRequestSort}
handlePageChange={handlePageChange}
handleRowsPerPageChange={handleRowsPerPageChange}
handleNestedAction={handleNestedAction}
handleNestedFieldChange={handleNestedFieldChange}
handleActionClick={(event, key) => handleAction(event, key)}
actionButtons={["create", "edit", "delete", "filter", "refresh"]}
handleUniversalFilterChange={handleUniversalFilterChange}
nestedRowAction={nestedRowAction}
></EnhancedTable>
);
}
Prop | Type | Default | Description |
---|---|---|---|
title | string |
undefined |
Table title |
rows | array |
required | Table row data |
headers | array |
required | Table headers. Customise with props (see below) |
dense | bool |
true |
Whether to use dense prop for Material UI's Table |
order | string |
asc |
Column sort order. One of :asc ,desc |
orderBy | string |
id |
Attribute to sort column by |
loading | bool |
false |
Whether to display loading Backdrop component |
page | number |
1 |
Current page |
totalCount | number |
undefined |
Total result count |
rowsPerPage | number |
10 |
Number of rows per page |
selected | array |
[] |
Selected row |
descriptorAttribute | string |
undefined | Attributed used to display descriptor for selected row in toolbar |
handleRowClick | func |
() => {} |
Method to handle row click event |
handleRequestSort | func |
() => {} |
Method to handle column sort click |
handlePageChange | func |
undefined |
Method to handle page change |
handleRowsPerPageChange | func |
undefined |
Method to handle rows per page change |
handleUniversalFilterChange | func |
() => {} |
Method to handle universal filter onChange event |
handleDateChange | func |
() => {} |
Method to handle date changes in date filters |
handleActionClick | func |
() => {} |
Method to handle table action click |
handleNestedAction | func |
() => {} |
Method to handle action click within a nested table in a row |
handleNestedFieldChange | func |
() => {} |
Method to handle nested field onChange event |
nestedRowAction | object |
{} |
Object indicating the actions available for each nested row. Possible actions include add or edit . Object needs to be of the form { [nestedRowIdentifier]: { add: true, edit: true } } |
date | string |
undefined | Filter dates. Must be in the form yyyy-MM-dd |
dates | object |
undefined | Filter dates. Must be in the form { from: "yyyy-MM-dd", to: "yyyy-MM-dd" } |
actionButtons | array |
["create", "edit", "delete", "filter"] |
Actions to include in table |
showToolbar | bool |
true |
Whether to show the toolbar |
collapsible | bool |
false |
Whether each row should be collapsible |
collapseContent | array |
null |
Array of content for each collapsible row. Index of this array should correspond to index of rows. The collapse content for rows[0] should be collapseContent[0] . Default collapse content is a table. |
collapseHeaders | array |
[] |
Headers for default table within collapse content. Required if collapsible = true and collapseContent prop is not passed |
openRows | object |
{} |
Object to indicate which collapsible rows should be open. Object should be of the form { [row[identifier]]: true } |
identifier | string |
id |
Attribute used as row identifier |
handleCollapseIconClick | func |
() => {} |
Method to handle collapse icon click event |
disableRowClick | bool |
false |
Whether to ignore click event on row |
disableSelection | bool |
false |
Makes rows unselectable |
selectibleRows | array |
null |
Manually define the selectible rows. Array should contain the row identifiers |
refreshBadgeCount | number |
0 |
Badge count for refresh button. This can be used to indicate whether the table has pending unfetched data |
Prop | Type | Default | Description |
---|---|---|---|
key | string |
attribute |
Attribute used to determine row key |
attribute | string |
undefined |
Attribute used to determine cell content |
label | string |
undefined |
Header label |
multiField | bool |
undefined |
Whether the cell should display content from multiple attributes. Best used with data and html props. |
multiFieldData | array |
undefined |
Array of attributes used for multiField content. Required if multiField = true . Array should be of the form [{ attribute: "name" }] |
html | string |
undefined |
HTML code for displaying content. Attribute will be substituted in for {{0}} . If used with multiField , data attributes will be substituted in for {{index}} , where index is the position index of the data array |
chip | bool |
undefined |
If true, column will contain a Material UI Chip populated with attribute . The color prop can be used to determine the Chip's color |
chipColor | object |
undefined |
Used with chip . Object of the form { [option]: [bgColor, borderColor] } |
collapse | bool |
undefined |
If true, column will contain a collapse icon |
checkbox | bool |
undefined |
If true, column will contain a Material UI Checkbox component that is controlled by whether the row is selected |
actions | array |
undefined |
Array containing possible actions for each row. Should be of the form { id: "cancel", icon: <Block />, tooltip: "Cancel", onClick: (event, row) => {}, color: indigo[400], hideCondition: (data) => {} } |
headerActions | array |
undefined |
Array containing possible actions for the header. Should be of the form { id: 'edit', icon: <Edit />, tooltip: 'Edit', onClick: (event) => {}, hideCondition: false, color: indigo[400]} |
component | func |
undefined |
Custom component for cell content. Row data is passed as sole parameter. |
stopPropagation | bool |
undefined |
Used with component . If true, cell component click will stop further propagation to parent row. |
arrayAttribute | string |
undefined |
Attribute used to populate nested table within row |
childAttribute | string |
undefined |
Attribute within arrayAttribute object to display as nested row |
childAttribute2 | string |
undefined |
Attribute within arrayAttribute object to display on left of childAttribute within nested row |
childLabelAttribute | string |
undefined |
Attribute within arrayAttribute object to display on left of childAttribute and childAttribute2 within nested row |
childAttributeLabel | string |
undefined |
Label for childAttribute |
childAttribute2Label | string |
undefined |
Label for childAttribute2 |
childLabelAttributeLabel | string |
undefined |
Label for childLabelAttribute |
childActions | object |
undefined |
Object indicating which actions to show for each nested row. Actions available are add , edit and delete . Should be of the form { delete: true, edit: true, add: true } . |
orderBy^ | array |
undefined |
Attribute to order nested table content. Should be of the form [attribute, asc|desc] |
numeric | bool |
undefined |
If true, content will be right-aligned |
price | bool |
undefined |
If true, content will be prefixed with $ |
date | bool |
undefined |
If true, content will be parsed with date-fns in the format d MMM yyyy |
datetime | bool |
undefined |
If true, content will be parsed with date-fns in the format d MMM yyyy and h:mm:ss a |
time | bool |
undefined |
If true, content will be parsed with date-fns in the format h:mm:ss a |
truncate | number |
undefined |
Max length of string content |
width | number |
undefined |
Fixed width for column |
minWidth | number |
undefined |
Minimum width for column |
maxWidth | number |
undefined |
Maximum width for column |
disablePadding | bool |
undefined |
If true, cell padding will be set to 0 |
sortable | bool |
undefined |
If true, column will be sortable based on attribute . attribute is required for column to be sortable |
^Only for nested table