How to make collapsible list in sidebar

Viewed 175

I'm trying to make collapsible list in sidebar. On click i'm changing "isOpen" state and depending on this state I'm displaying or hiding sub links. The problem is that all sub links opening at same time .

Check sandbox here: https://codesandbox.io/s/infallible-moore-h16g6

const Sidebar = ({ title, children, data, opened, ...attrs }) => {
  const [isOpen, setTriger] = useState(false);
  const handleClick = idx => {
    setTriger(!isOpen)
  };

  return (
    <SidebarUI>
      {data.map((item, idx) => {
        return typeof item.data === "string" ?
        <div key={idx} >{item.name}</div>:
          <Fragment key={idx}>
            <div onClick={() => handleClick(idx)}>{item.name}</div>
            { item.data.map((subs, ids) => {
              return <Test isOpen={isOpen} key={ids}>++++{subs.name}</Test>;
            })}
          </Fragment>
      })}
    </SidebarUI>
  );
};
4 Answers

Try creating an object with the state of the collapsed elements, like this:

const Sidebar = ({ title, children, data, opened, ...attrs }) => {
  const [collapseElements, setCollapse] = useState({});

  const handleClick = idx => {
    const currentElements = Object.assign({}, collapseElements);

    setCollapse({ ...currentElements, [idx]: !collapseElements[idx] });
  };

  return (
    <SidebarUI>
      {data.map((item, idx) => {
        return typeof item.data === "string" ? (
          <div key={idx}>{item.name}</div>
        ) : (
          <Fragment key={idx}>
            <div onClick={() => handleClick(idx)}>{item.name}</div>

            {item.data.map((subs, ids) => {
              return (
                <Test isOpen={collapseElements[idx]} key={ids}>
                  ++++{subs.name}
                </Test>
              );
            })}
          </Fragment>
        );
      })}
    </SidebarUI>
  );
};
export default Sidebar;

Checkout the sandbox.

Let me know if it helps.

Edit:

I made a new codesandbox wich I added some transitions. Now open and closing are smooth.


Check this codesandbox

Now it opens and closes.

What you have to do is keep what index you clicked and only display the children when it's the same index.

I also added a way to close and open.

Here is doing that with the code from your question.

const Sidebar = ({ title, children, data, opened, ...attrs }) => {
  const [openedIndex , setTriger] = useState(false);
  const handleClick = idx => {
    // this ternary makes it possible to open and close 
    setTriger(idx === openedIndex ? -1 : idx)
  };

  return (
    <SidebarUI>
      {data.map((item, idx) => {
        return typeof item.data === "string" ?
        <div key={idx} >{item.name}</div>:
          <Fragment key={idx}>
            <div onClick={() => handleClick(idx)}>{item.name}</div>
            {// here you check if the idx is the same as the opened one}
            {// before showing the data of the item}
            {idx === openedIndex && item.data.map((subs, ids) => {
              return <Test isOpen={true} key={ids}>++++{subs.name}</Test>;
            })}
          </Fragment>
      })}
    </SidebarUI>
  );
};

You can solve this without using state. Try changing to this

<Fragment key={idx}>
  <div class="sidebar-item" onClick={e => openSidebar(e)}>
    {item.name}
  </div>

  {item.data.map((subs, ids) => {
   return (
      <div className="sidebar-subitem" key={ids}>
        ++++{subs.name}
      </div>
    );
  })}
</Fragment>

Toggle class on click

 function openSidebar(e) {
    e.preventDefault();
    e.target.classList.toggle("open");
  }

Add css

.sidebar-subitem {
  display: none;
}

.sidebar-item.open + .sidebar-subitem {
  display: block;
}

This is of course not better than @axeljunes but will work too, so I've maintained a separate list of toggled id's and based on that it will toggle. Also this is my first time using hooks so bare me(feel free to correct me)

const Sidebar = ({ title, children, data, opened, ...attrs }) => {
  //const [isOpen, setTriger] = useState(false);
  const [list, setList] = useState([]);

  const handleClick = idx => {
    //setTriger(!isOpen);
    if(!list.includes(idx))
      setList([...list,idx]);
    else{      
      const newList = list.filter(e => e!==idx);
      setList(newList);
    }

  };
  return (
    <SidebarUI>
      {data.map((item, idx) => {
        return typeof item.data === "string" ? (
          <div key={idx}>{item.name}MAIN</div>
        ) : (
          <Fragment key={idx}>
            <div onClick={() => handleClick(idx)}>{item.name}IN</div>

            {item.data.map((subs, ids) => {
              return (
                <Test isOpen={list.includes(idx)} key={ids}>
                  ++++{subs.name}SIDE
                </Test>
              );
            })}
          </Fragment>
        );
      })}
    </SidebarUI>
  );
};
export default Sidebar;
Related