0

I have a problem selecting a single checkbox or multiple checkbox in a table in React. I'm using Material-UI. Please see my codesandbox here

CLICK HERE

I wanted to achieve something like this in the picture below:

enter image description here

  <TableContainer className={classes.tableContainer}>
    <Table>
      <TableHead className={classes.tableHead}>
        <TableRow>
          <TableCell padding="checkbox">
            <Checkbox
              checked={false}
              inputProps={{ "aria-label": "select all desserts" }}
            />
          </TableCell>
          {head.map((el) => (
            <TableCell key={el} align="left">
              {el}
            </TableCell>
          ))}
        </TableRow>
      </TableHead>
      <TableBody>
        {body?.excluded_persons?.map((row, index) => (
          <TableRow key={row.id}>
            <TableCell padding="checkbox">
              <Checkbox checked={true} />
            </TableCell>
            <TableCell align="left">{row.id}</TableCell>
            <TableCell align="left">{row.name}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  </TableContainer>
Joseph
  • 5,548
  • 14
  • 57
  • 125
  • It looks to me like you're not even attempting to use state management. The checkboxes are probably stuck because you're hardcoding their checked values like so: – Inbar Koursh Jul 31 '21 at 12:14
  • @Inbar Koursh. sorry I just deleted it cause I'm not sure if my work is correct so as not to confuse people – Joseph Jul 31 '21 at 12:19

2 Answers2

1

Seems you are just missing local component state to track the checked status of each checkbox, including the checkbox in the table header.

Here is the implementation for the AddedPersons component since it's more interesting because it has more than one row of data.

  1. Create state to hold the selected persons state. Only add the additional local state, no need to duplicate the passed body prop data (this is anti-pattern anyway) nor add any derived state, i.e. is indeterminate or is all selected (also anti-pattern).

    const [allSelected, setAllSelected] = React.useState(false);
    const [selected, setSelected] = React.useState({});
    
  2. Create handlers to toggle the states.

    const toggleAllSelected = () => setAllSelected((t) => !t);
    
    const toggleSelected = (id) => () => {
      setSelected((selected) => ({
        ...selected,
        [id]: !selected[id]
      }));
    };
    
  3. Use a useEffect hook to toggle all the selected users when the allSelected state is updated.

    React.useEffect(() => {
      body.persons?.added_persons &&
        setSelected(
          body.persons.added_persons.reduce(
            (selected, { id }) => ({
              ...selected,
              [id]: allSelected
            }),
            {}
          )
        );
    }, [allSelected, body]);
    
  4. Compute the selected person count to determine if all users are selected manually or if it is "indeterminate".

    const selectedCount = Object.values(selected).filter(Boolean).length;
    
    const isAllSelected = selectedCount === body?.persons?.added_persons?.length;
    
    const isIndeterminate =
      selectedCount && selectedCount !== body?.persons?.added_persons?.length;
    
  5. Attach all the state and callback handlers.

    return (
      <>
        <TableContainer className={classes.tableContainer}>
          <Table>
            <TableHead className={classes.tableHead}>
              <TableRow>
                <TableCell colSpan={4}>{selectedCount} selected</TableCell>
              </TableRow>
              <TableRow>
                <TableCell padding="checkbox">
                  <Checkbox
                    checked={allSelected || isAllSelected}      // <-- all selected
                    onChange={toggleAllSelected}                // <-- toggle state
                    indeterminate={isIndeterminate}             // <-- some selected
                    inputProps={{ "aria-label": "select all desserts" }}
                  />
                </TableCell>
                ...
              </TableRow>
            </TableHead>
            <TableBody>
              {body?.persons?.added_persons?.map((row, index) => (
                <TableRow key={row.id}>
                  <TableCell padding="checkbox">
                    <Checkbox
                      checked={selected[row.id] || allSelected} // <-- is selected
                      onChange={toggleSelected(row.id)}         // <-- toggle state
                    />
                  </TableCell>
                  <TableCell align="left">{row.id}</TableCell>
                  <TableCell align="left">{row.name}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
      </>
    );
    

Update

Seems there was a bug in my first implementation that disallowed manually deselecting people while the select all checkbox was checked. The fix is to move the logic in the useEffect into the toggleAllSelected handler and use the onChange event to toggle all the correct states. Also to add a check to toggleSelected to deselect "select all" when any person checkboxes have been deselected.

const [allSelected, setAllSelected] = React.useState(false);
const [selected, setSelected] = React.useState({});

const toggleAllSelected = (e) => {
  const { checked } = e.target;
  setAllSelected(checked);

  body?.persons?.added_persons &&
    setSelected(
      body.persons.added_persons.reduce(
        (selected, { id }) => ({
          ...selected,
          [id]: checked
        }),
        {}
      )
    );
};

const toggleSelected = (id) => (e) => {
  if (!e.target.checked) {
    setAllSelected(false);
  }

  setSelected((selected) => ({
    ...selected,
    [id]: !selected[id]
  }));
};

enter image description here enter image description here enter image description here

Edit selection-checkbox-in-react-using-hooks

Note: Since both AddedPersons and ExcludedPersons components are basically the same component, i.e. it's a table with same headers and row rendering and selected state, you should refactor these into a single table component and just pass in the row data that is different. This would make your code more DRY.

Drew Reese
  • 103,803
  • 12
  • 69
  • 96
  • @Joseph Fixed. `isIndeterminate` is now coerced to boolean type, and you had an extra header column, so I removed that, there are now three table columns. Ah yeah, and I see now the bug issue, looking into that. – Drew Reese Aug 01 '21 at 02:39
  • There still seems a bug on first table. click the main checkbox that checks all checkboxes. and then click a single checkbox, it doesnt uncheck – Joseph Aug 01 '21 at 02:47
  • @Joseph Resolved the deselecting issue. Getting the second table (excluded persons) working was covered by my included note in answer. Since the two components are basically identical it makes more sense to refactor the above code to take an array of row data and be used in both places than it does to duplicate the logic into two components. If you need help with this, or if what I'm describing isn't making sense then let me know. – Drew Reese Aug 01 '21 at 02:58
  • @Joseph Here's a [forked codesandbox](https://codesandbox.io/s/selection-checkbox-in-react-using-hooks-forked-5cyve?file=/components/Table/index.js) with a single table component with different data passed as props to it. The row data is *just* data, so moving row data from one to the other should be fairly trivial. – Drew Reese Aug 01 '21 at 03:28
  • Hi again Drew. Do you know this? This is in NextJS by the way. https://stackoverflow.com/questions/68450181/show-more-based-on-height-in-react – Joseph Aug 01 '21 at 05:46
0

I have updated your added person table as below,

please note that I am using the component state to update the table state,

const AddedPersons = ({ classes, head, body }) => {

  const [addedPersons, setAddedPersons] = useState(
    body?.persons?.added_persons.map((person) => ({
      ...person,
      checked: false
    }))
  );

  const [isAllSelected, setAllSelected] = useState(false);
  const [isIndeterminate, setIndeterminate] = useState(false);

  const onSelectAll = (event) => {
    setAllSelected(event.target.checked);
    setIndeterminate(false);
    setAddedPersons(
      addedPersons.map((person) => ({
        ...person,
        checked: event.target.checked
      }))
    );
  };

  const onSelect = (event) => {
    const index = addedPersons.findIndex(
      (person) => person.id === event.target.name
    );
    // shallow clone
    const updatedArray = [...addedPersons];
    updatedArray[index].checked = event.target.checked;
    setAddedPersons(updatedArray);

    // change all select checkbox
    if (updatedArray.every((person) => person.checked)) {
      setAllSelected(true);
      setIndeterminate(false);
    } else if (updatedArray.every((person) => !person.checked)) {
      setAllSelected(false);
      setIndeterminate(false);
    } else {
      setIndeterminate(true);
    }
  };

  const numSelected = addedPersons.reduce((acc, curr) => {
    if (curr.checked) return acc + 1;
    return acc;
  }, 0);

  return (
    <>
      <Toolbar>
        {numSelected > 0 ? (
          <Typography color="inherit" variant="subtitle1" component="div">
            {numSelected} selected
          </Typography>
        ) : (
          <Typography variant="h6" id="tableTitle" component="div">
            Added Persons
          </Typography>
        )}
      </Toolbar>
      <TableContainer className={classes.tableContainer}>
        <Table>
          <TableHead className={classes.tableHead}>
            <TableRow>
              <TableCell padding="checkbox">
                <Checkbox
                  checked={isAllSelected}
                  inputProps={{ "aria-label": "select all desserts" }}
                  onChange={onSelectAll}
                  indeterminate={isIndeterminate}
                />
              </TableCell>
              {head.map((el) => (
                <TableCell key={el} align="left">
                  {el}
                </TableCell>
              ))}
            </TableRow>
          </TableHead>
          <TableBody>
            {addedPersons?.map((row, index) => (
              <TableRow key={row.id}>
                <TableCell padding="checkbox">
                  <Checkbox
                    checked={row.checked}
                    onChange={onSelect}
                    name={row.id}
                  />
                </TableCell>
                <TableCell align="left">{row.id}</TableCell>
                <TableCell align="left">{row.name}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </>
  );
};

export default AddedPersons;

Please refer to this for a working example: https://codesandbox.io/s/redux-react-forked-cuy51

Nadun
  • 6,223
  • 1
  • 20
  • 26
  • Thanks for this. I think checkbox for second table is not working in your codesandbox – Joseph Jul 31 '21 at 15:18
  • Yes. That's because I didn't add the logic for the second table. You can reuse the same logic for the second table as well. – Nadun Jul 31 '21 at 15:20
  • You also forgot to add these. https://ibb.co/zhyX3pN. – Joseph Jul 31 '21 at 15:27
  • please check the answer I updated it. However, when I was going through the documentation I found these examples: https://material-ui.com/components/tables/ please go through these as well. – Nadun Jul 31 '21 at 16:10