codemebro
codemebro

Reputation: 173

How to create dynamic nested accordion in React

How i can create an accordion like this

-parent
   -subparent1
      -subparent2
        ...
          -subparentN
             - child

Here is my data.

//parents
{id: 1, name: "", parent_id: null}
{id: 2, name: "", parent_id: null }
{id: 3, name: "", parent_id: null }
{id: 4, name: "", parent_id: null}

//children
{id: 5, name: "", parent_id: 1}
{id: 6, name: "", parent_id: 1}
{id: 7, name: "", parent_id: 5}
{id: 8, name: "", parent_id: 5}
{id: 9, name: "", parent_id: 6}
{id: 10, name: "", parent_id: 6}
{id: 11, name: "", parent_id: 6}
{id: 12, name: "", parent_id: 6}
{id: 13,name: "", parent_id: 6}
{id: 14, name: "", parent_id: 6}

Basically the ones who have parent_id:null are parents, and when I click on them I want their potential children to be displayed if they have any, now this isn't that hard, but what I can't understand is how to display subparent's children

Upvotes: 1

Views: 6486

Answers (3)

Stavros Angelis
Stavros Angelis

Reputation: 962

I think your data structure should be a nested object similarly to how you see your menu working, i.e.

[
    {id: 1, name:"", children: [
      {id: 5, name: "", children: []},
      {id: 6, name: "", children: [
        {id: 7, name: "", children: []},
        {id: 8, name: "", children: []},
      ]},
    ]},
    {id: 2, name:"", children:[]}
]

Then you would need a function to output each item:

const returnMenuItem = (item, i) =>{
  let menuItem;

  if (item.children.length===0) {
    menuItem = <div key={i}>{item.label}</div>;
  }
  else {
    let menuItemChildren = item.children.map((item,i)=>{
      let menuItem = returnMenuItem(item,i);
      return menuItem;
    });
    menuItem = <div key={i}>
      <div>{item.label}</div>
      <div>
        {menuItemChildren}
      </div>
    </div>;
  }
  return menuItem;
}

And you would invoke this function by going through your items:

let menuItems = data.map((item,i)=>{
    let menuItem = returnMenuItem(item,i);
    return menuItem;
});

A complete component would look something like the following:

import React, { useState, useEffect } from "react";
import { UncontrolledCollapse } from "reactstrap";

const Menu = (props) => {

  const [loading, setLoading] = useState(true);
  const [items, setItems] = useState([]);

  useEffect(() => {
    const menuData = [
      {
        id: 1,
        name: "test 1",
        children: [
          { id: 5, name: "test 5", children: [] },
          {
            id: 6,
            name: "test 6",
            children: [
              { id: 7, name: "test 7", children: [] },
              { id: 8, name: "test 8", children: [] }
            ]
          }
        ]
      },
      { id: 2, name: "test 2", children: [] }
    ];
    const returnMenuItem = (item, i) => {
      let menuItem;

      if (item.children.length === 0) {
        menuItem = (
          <div className="item" key={i}>
            {item.name}
          </div>
        );
      } else {
        let menuItemChildren = item.children.map((item, i) => {
          let menuItem = returnMenuItem(item, i);
          return menuItem;
        });
        menuItem = (
          <div key={i} className="item">
            <div className="toggler" id={`toggle-menu-item-${item.id}`}>
              {item.name}
            </div>
            <UncontrolledCollapse
              className="children"
              toggler={`#toggle-menu-item-${item.id}`}
            >
              {menuItemChildren}
            </UncontrolledCollapse>
          </div>
        );
      }
      return menuItem;
    };

    const load = async () => {
      setLoading(false);
      let menuItems = menuData.map((item, i) => {
        let menuItem = returnMenuItem(item, i);
        return menuItem;
      });
      setItems(menuItems);
    };
    if (loading) {
      load();
    }
  }, [loading]);

  return <div className="items">{items}</div>;
};
export default Menu;

And a minimum css:

.item {
  display: block;
}
.item > .children {
  padding: 0 0 0 40px;
}
.item > .toggler {
  display: inline-block;
}

.item::before {
  content: "-";
  padding: 0 5px 0 0;
}

You can find a working code sandbox here sandbox

Upvotes: 0

Reyno
Reyno

Reputation: 6525

You can loop over the list of all your items and add each sub item to their parent. Afterwards you just have to loop over all the items in the array and create their respective html.

const items = [
  //parents
  {id: 1, name: "1", parent_id: null},
  {id: 2, name: "2", parent_id: null },
  {id: 3, name: "3", parent_id: null },
  {id: 4, name: "4", parent_id: null},

  //children
  {id: 5, name: "5", parent_id: 1},
  {id: 6, name: "6", parent_id: 1},
  {id: 7, name: "7", parent_id: 5},
  {id: 8, name: "8", parent_id: 5},
  {id: 9, name: "9", parent_id: 6},
  {id: 10, name: "10", parent_id: 6},
  {id: 11, name: "11", parent_id: 6},
  {id: 12, name: "12", parent_id: 6},
  {id: 13,name: "13", parent_id: 6},
  {id: 14, name: "14", parent_id: 6},
];

for(const item of items) {
  // Find the parent object
  const parent = items.find(({ id }) => id === item.parent_id);
  // If the parent is found add the object to its children array
  if(parent) {
    parent.children = parent.children ? [...parent.children, item] : [item]
  }
};

// Only keep root elements (parents) in the main array
const list = items.filter(({ parent_id }) => !parent_id);

// console.log(list);

// Show tree (vanillaJS, no REACT)
for(const item of list) {
  // Create a new branch for each item
  const ul = createBranch(item);
  // Append branch to the document
  document.body.appendChild(ul);
}

function createBranch(item) {
  // Create ul item for each branch
  const ul = document.createElement("ul");
  // Add current item as li to the branch
  const li = document.createElement("li");
  li.textContent = item.name;
  ul.appendChild(li);

  // Check if there are children
  if(item.children) {
    // Create a new branch for each child
    for(const child of item.children) {
      const subUl = createBranch(child);
      // Append child branch to current branch
      ul.appendChild(subUl);
    }
  }
  
  return ul;
}
ul {
  margin: 0;
  padding-left: 2rem;
}

Upvotes: 1

Stefan Lazarević
Stefan Lazarević

Reputation: 37

I think that your data structure has a flaw. Beside child to parent relation, you should keep track of parent to child relationship. Now you will be able to easily iterate through the data and render sub parent's children.

{id: 1, parent_id: null, children: [
  {id: 2, parent_id: 1, children: []},
  {id: 3, parent_id: 1, children: [
    {id: 4, parent_id: 3, children: []}
  ]}
]}

If you need to keep all objects inline, you can structure your data like:

{id: 1, parent_id: null, children: [2, 3]}
{id: 2, parent_id: 1, children: []},
{id: 3, parent_id: 1, children: [4]},
{id: 4, parent_id: 3, children: []}

Upvotes: 0

Related Questions