skip to Main Content

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.

enter image description here

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


  1. You need to alter your descendingComparator function to behave differently when it is asked to sort by the name field.

    You have two options:

    1. Add another field to each row so the name column can sort by that field instead of the displayed field.
    2. Change the logic to remove everything before the > character in the name string before sorting.
    Login or Signup to reply.
  2. Here’s an ad-hoc solution. Modify the descendingComparator function like this.

    function descendingComparator(a, b, orderBy) {
      const aValue = orderBy === 'name' ? a[orderBy].props.children : a[orderBy]
      const bValue = orderBy === 'name' ? b[orderBy].props.children : b[orderBy]
    
      if (bValue < aValue) {
        return -1
      }
      if (bValue > aValue) {
        return 1
      }
      return 0
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search