Search code examples
javascriptreactjsreact-bootstrap

Toggle selected/active state with react-bootstrap ListGroup


I'm building a multiple selection in React with a react-bootstrap ListGroup.

Despite having the active property of an item set to false though, the last clicked item still gets active in its class list.

I've been considering getting a reference to the list item and manipulate the class list, but that is too dirty.

I tried changing the variant attribute of the ListGroup.Item, but the active class overrides that. I'd prefer to not modify the css class definition.

I prefer using the onSelect handler of the ListGroup instead of using the ToggleButton's onClick event from a usability perspective.

Have you tried using a ListGroup to manipulate multiple selection?

Here is stripped version of the code:

import { useState } from "react";
import "./styles.css";
import ListGroup from "react-bootstrap/ListGroup";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import ToggleButton from "react-bootstrap/ToggleButton";
import "bootstrap/dist/css/bootstrap.min.css";
import { cloneDeep } from "lodash";

export default function App() {
  const allItems = [
    { id: 1, title: "A" },
    { id: 2, title: "B" },
    { id: 3, title: "C" },
    { id: 4, title: "D" },
    { id: 5, title: "E" }
  ];
  const [selectedItems, setSelectedItems] = useState([
    { id: 2, title: "B" },
    { id: 4, title: "D" }
  ]);
  function toggleSelection(eventKey, e) {
    const itemId = parseInt(eventKey?.replace(/^itemSelect_/, ""), 10);

    const newSelectedItems = cloneDeep(selectedItems);
    const indexInSelection = selectedItems.findIndex(
      (sitm) => sitm.id === itemId
    );

    if (indexInSelection >= 0) {
      newSelectedItems.splice(indexInSelection, 1);
    } else {
      const newItem = allItems.find((itm) => itm.id === itemId);
      newSelectedItems.push(newItem);
    }
    setSelectedItems(newSelectedItems);
  }
  return (
    <div className="App">
      <ListGroup onSelect={toggleSelection}>
        {allItems.map((itm) => {
          return (
            <ListGroup.Item
              eventKey={`itemSelect_${itm.id}`}
              key={`itemSelect${itm.id}`}
              active={
                selectedItems.find((sitm) => sitm.id === itm.id) !== undefined
              }
            >
              <Container>
                <Row>
                  <Col sm="2">
                    {" "}
                    <ToggleButton
                      className="mx-1"
                      type="checkbox"
                      size="sm"
                      key={`check${itm.id}`}
                      checked={
                        selectedItems.find((sitm) => sitm.id === itm.id) !==
                        undefined
                      }
                      value={itm.id}
                    ></ToggleButton>
                  </Col>
                  <Col sm="10">{itm.title}</Col>
                </Row>
              </Container>
            </ListGroup.Item>
          );
        })}
      </ListGroup>
    </div>
  );
}

And a live preview is available at Code Sandbox

Thanks in advance for your help!


Solution

  • It's something to do with eventKey and onSelect - ListGroup and ListGroup.Item have some internal implementations on various user actions as you can see in docs. Try this implementation instead. I removed onSelect on ListGroup and added onClick event on ListGroup.Item where we don't need eventKey anymore and pass itm.id directly.