I am building a reactjs checkbox table and noticed that the sorting has stopped working if the cell contains links. Is there a way to append data attributes to the cell wrapper to use as means of sorting rather than just the inner contents?
There are also problems trying to make more complicated mixed markup arrangements, where the contents returns as an object instead.
https://codesandbox.io/s/vigilant-ace-x7c478?file=/src/index.js
//usage
let table = {
header: 'Patients',
headCells: [
{
id: 'id',
numeric: false,
disablePadding: true,
label: 'Id',
},
{
id: 'name',
numeric: false,
disablePadding: true,
label: 'NameĀ ',
},
{
id: 'saturation',
numeric: true,
disablePadding: false,
label: 'SaturationĀ (%)',
},
],
rows: [
{id: '123', name: <a href={11}>Tiffany Cruz</a>, saturation: '10%'},
{id: '124', name: <a href={1}>Aaron Williams</a>, saturation: '16%'},
{id: '125', name: <a href={2}>Estelie Balley</a>, saturation: '30%'},
{id: '126', name: <a href={3}>Stephen Fries</a>, saturation: '40%'},
{id: '127', name: <a href={4}>Jessica Alma</a>, saturation: '2%'},
{id: '128', name: <a href={5}>Brian Pax</a>, saturation: '3%'},
],
otherActions: [
{
action: 'filter',
icon: <FilterListIcon />,
},
],
}
<CheckboxTable
data={table}
handleAction={(action, selectedItems) =>
console.log('data', action, selectedItems)
}
/>
checkboxtable code
import * as React from 'react';
import PropTypes from 'prop-types';
import { alpha } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow';
import TableSortLabel from '@mui/material/TableSortLabel';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import DeleteIcon from '@mui/icons-material/Delete';
import FilterListIcon from '@mui/icons-material/FilterList';
import { visuallyHidden } from '@mui/utils';
import './CheckboxTable.scss';
function descendingComparator(a, b, orderBy) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
function getComparator(order, orderBy) {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
// Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
// stableSort() brings sort stability to non-modern browsers (notably IE11). If you
// only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
// with exampleArray.slice().sort(exampleComparator)
function stableSort(array, comparator) {
const stabilizedThis = array.map((el, index) => [el, index]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) {
return order;
}
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}
function EnhancedTableHead(props) {
const { onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort } =
props;
const createSortHandler = (newOrderBy) => (event) => {
onRequestSort(event, newOrderBy);
};
return (
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
color="primary"
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
inputProps={{
'aria-label': 'select all desserts',
}}
/>
</TableCell>
{props.headCells.map((headCell) => (
<TableCell
key={headCell.id}
className={"head-cell-"+headCell.id}
align={headCell.numeric ? 'right' : 'left'}
padding={headCell.disablePadding ? 'none' : 'normal'}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}
EnhancedTableHead.propTypes = {
numSelected: PropTypes.number.isRequired,
onRequestSort: PropTypes.func.isRequired,
onSelectAllClick: PropTypes.func.isRequired,
order: PropTypes.oneOf(['asc', 'desc']).isRequired,
orderBy: PropTypes.string.isRequired,
rowCount: PropTypes.number.isRequired,
};
function EnhancedTableToolbar(props) {
const { numSelected } = props;
return (
<Toolbar
sx={{
pl: { sm: 2 },
pr: { xs: 1, sm: 1 },
...(numSelected > 0 && {
bgcolor: (theme) =>
alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity),
}),
}}
>
{numSelected > 0 ? (
<Typography
sx={{ flex: '1 1 100%' }}
color="inherit"
variant="subtitle1"
component="div"
>
{numSelected} selected
</Typography>
) : (
<Typography
sx={{ flex: '1 1 100%' }}
variant="h6"
id="tableTitle"
component="div"
>
{props.header}
</Typography>
)}
{numSelected > 0 ? (
<>
{props.otherActions &&
props.otherActions.map((item, j) => {
return(
<Tooltip key={j} title={item.label}>
<IconButton aria-label={item.action} onClick={(event) => props.handleAction(item.action, props.selected)}>
{item.icon}
</IconButton>
</Tooltip>
)
})
}
</>
) : (
<Tooltip title="Filter list">
<IconButton>
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
}
EnhancedTableToolbar.propTypes = {
numSelected: PropTypes.number.isRequired,
};
export default function EnhancedTable(props) {
//metaData
let DEFAULT_ORDER = 'asc';
let DEFAULT_ORDER_BY = 'name';
let DEFAULT_ROWS_PER_PAGE = 10;
let DEFAULT_DENSE = true;
if(props.data?.metaData){
DEFAULT_ORDER = props.data.metaData.DEFAULT_ORDER;
DEFAULT_ORDER_BY = props.data.metaData.DEFAULT_ORDER_BY;
DEFAULT_ROWS_PER_PAGE = props.data.metaData.DEFAULT_ROWS_PER_PAGE;
DEFAULT_DENSE = props.data.metaData.DEFAULT_DENSE;
}
const [order, setOrder] = React.useState(DEFAULT_ORDER);
const [orderBy, setOrderBy] = React.useState(DEFAULT_ORDER_BY);
const [selected, setSelected] = React.useState([]);
const [page, setPage] = React.useState(0);
const [dense, setDense] = React.useState(DEFAULT_DENSE);
const [visibleRows, setVisibleRows] = React.useState(null);
const [rowsPerPage, setRowsPerPage] = React.useState(DEFAULT_ROWS_PER_PAGE);
const [paddingHeight, setPaddingHeight] = React.useState(0);
let previousProps = React.useRef(props);
React.useEffect(() => {
//////////////
if(previousProps.data !== props.data) {
//console.log("old", previousProps.data)
setPage(0);//if the row length has changed..
setSelected([]);
}
previousProps = props;
///////////
let rowsOnMount = stableSort(
props.data.rows,
getComparator(DEFAULT_ORDER, DEFAULT_ORDER_BY),
);
rowsOnMount = rowsOnMount.slice(
0 * DEFAULT_ROWS_PER_PAGE,
0 * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
);
setVisibleRows(rowsOnMount);
}, [props]);
const handleRequestSort = React.useCallback(
(event, newOrderBy) => {
const isAsc = orderBy === newOrderBy && order === 'asc';
const toggledOrder = isAsc ? 'desc' : 'asc';
setOrder(toggledOrder);
setOrderBy(newOrderBy);
const sortedRows = stableSort(props.data.rows, getComparator(toggledOrder, newOrderBy));
const updatedRows = sortedRows.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage,
);
setVisibleRows(updatedRows);
},
[order, orderBy, page, rowsPerPage, props],
);
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelected = props.data.rows.map((n) => n.id);
setSelected(newSelected);
return;
}
setSelected([]);
};
const handleClick = (event, id) => {
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),
);
}
setSelected(newSelected);
};
const handleChangePage = React.useCallback(
(event, newPage) => {
setPage(newPage);
const sortedRows = stableSort(props.data.rows, getComparator(order, orderBy));
const updatedRows = sortedRows.slice(
newPage * rowsPerPage,
newPage * rowsPerPage + rowsPerPage,
);
setVisibleRows(updatedRows);
// Avoid a layout jump when reaching the last page with empty rows.
const numEmptyRows =
newPage > 0 ? Math.max(0, (1 + newPage) * rowsPerPage - props.data.rows.length) : 0;
const newPaddingHeight = (dense ? 33 : 53) * numEmptyRows;
setPaddingHeight(newPaddingHeight);
},
[order, orderBy, dense, rowsPerPage, props],
);
const handleChangeRowsPerPage = React.useCallback(
(event) => {
const updatedRowsPerPage = parseInt(event.target.value, 10);
setRowsPerPage(updatedRowsPerPage);
setPage(0);
const sortedRows = stableSort(props.data.rows, getComparator(order, orderBy));
const updatedRows = sortedRows.slice(
0 * updatedRowsPerPage,
0 * updatedRowsPerPage + updatedRowsPerPage,
);
setVisibleRows(updatedRows);
// There is no layout jump to handle on the first page.
setPaddingHeight(0);
},
[order, orderBy, props],
);
const handleChangeDense = (event) => {
setDense(event.target.checked);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
const renderCells = (labelId, row) => {
let stack = [];
for (let i = 0; i < Object.keys(row).length; ++i) {
if(i === 1){
stack.push(<TableCell key={i} className={"cell-"+Object.keys(row)[i]} component="th" id={labelId} scope="row" padding="none">{row[Object.keys(row)[i]]}</TableCell>)
} else {
stack.push(<TableCell key={i} className={"cell-"+Object.keys(row)[i]} align="right">{row[Object.keys(row)[i]]}</TableCell>)
}
}
return stack;
}
return (
<Box sx={{ width: '100%' }}>
<Paper sx={{ width: '100%', mb: 2 }}>
<EnhancedTableToolbar header={props.data.header} selected={selected} numSelected={selected.length} otherActions={props.data.otherActions} handleAction={props.handleAction} />
<TableContainer>
<Table
className="checkbox-table"
sx={{ minWidth: 750 }}
aria-labelledby="tableTitle"
size={dense ? 'small' : 'medium'}
>
<EnhancedTableHead
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={props.data.rows.length}
headCells={props.data.headCells}
/>
<TableBody>
{visibleRows
? visibleRows.map((row, index) => {
const isItemSelected = isSelected(row.id);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={(event) => handleClick(event, row.id)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.id}
selected={isItemSelected}
sx={{ cursor: 'pointer' }}
>
<TableCell padding="checkbox">
<Checkbox
color="primary"
checked={isItemSelected}
inputProps={{
'aria-labelledby': labelId,
}}
/>
</TableCell>
{/*
<TableCell
component="th"
id={labelId}
scope="row"
padding="none"
>
{row.name}
</TableCell>
<TableCell align="right">{row.calories}</TableCell>
<TableCell align="right">{row.fat}</TableCell>
<TableCell align="right">{row.carbs}</TableCell>
<TableCell align="right">{row.protein}</TableCell>
*/}
{
//console.log("row", row)
}
{
//console.log("key row", Object.keys(row))
}
{renderCells(labelId, row)}
</TableRow>
);
})
: null}
{paddingHeight > 0 && (
<TableRow
style={{
height: paddingHeight,
}}
>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={props.data.rows.length}
rowsPerPage={rowsPerPage}
page={props.data.rows.length <= rowsPerPage? 0 : page}
//page={!props.data.rows.length || props.data.rows.length <= 0 ? 0 : page}
//page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
{props.data?.metaData?.SWITCH_PADDING &&
<FormControlLabel
control={<Switch checked={dense} onChange={handleChangeDense} />}
label="Dense padding"
/>
}
</Box>
);
}
2
Answers
You need to alter your
descendingComparator
function to behave differently when it is asked to sort by thename
field.You have two options:
>
character in the name string before sorting.Here’s an ad-hoc solution. Modify the descendingComparator function like this.