React submenus from an array

Viewed 401

So i've made this navbar:

Looks like this

the navbar gets parameters such as URL, parentId from an array. All countys in the array have a non unique Id called parent which is equal to '1'. All towns have a non unique Id called parent which ranges from 2-22. In the case below Blekinge and Dalarna are countys (parent='1'), and their corresponding towns comes after in the array parent='2' are blekinge county towns and parent='3' are dalarna county towns. Below is a snippet of the array of countys and towns in sweden.

const orter = [{
        "id": "2",
        "rubrik": "Blekinge",
        "url": "blekinge",
        "parent": "1"
    },
    {
        "id": "23",
        "rubrik": "Karlshamn",
        "url": "blekinge/karlshamn",
        "parent": "2"
    },
    {
        "id": "24",
        "rubrik": "Karlskrona",
        "url": "blekinge/karlskrona",
        "parent": "2"
    },
    {
        "id": "25",
        "rubrik": "Olofström",
        "url": "blekinge/olofstrom",
        "parent": "2"
    },
    {
        "id": "26",
        "rubrik": "Ronneby",
        "url": "blekinge/ronneby",
        "parent": "2"
    },
    {
        "id": "27",
        "rubrik": "Sölvesborg",
        "url": "blekinge/solvesborg",
        "parent": "2"
    },
    {
        "id": "3",
        "rubrik": "Dalarna",
        "url": "dalarna",
        "parent": "1"
    },
    {
        "id": "28",
        "rubrik": "Älvdalen",
        "url": "dalarna/alvdalen",
        "parent": "3"
    },
    {
        "id": "29",
        "rubrik": "Avesta",
        "url": "dalarna/avesta",
        "parent": "3"
    },
    {
        "id": "318",
        "rubrik": "Borlänge",
        "url": "dalarna/borlange",
        "parent": "3"
    }]
export default orter

I want the sub-menu for each county to be populated with the corresponding towns. But I cant get this to work, what happens is instead that each county and town gets their own nav-link item and sub-menu item. I have tried mixing arround with if statements but I just cant wrap my head around it.

import React, {useState} from 'react'
import './nav.css'
import { Link } from 'react-router-dom'
import orter from '../../data/orter'

const Nav = () => {
    const [toggle, setToggle] = useState(false)
    
    const ort = orter.map((obj, key) =>{
        return (
                <ul className="nav-links">
                        <li 
                        parentid={obj.parent}
                        key={key}
                        >
                        <Link
                         to={obj.url}>
                            <i class='knapp'>#</i>
                            <span className="link_name">{obj.rubrik}</span>
                            <i className="arrow" onClick={()=> setToggle(!toggle)}>N</i>
                        </Link>
                        {toggle &&(
                            <ul className="sub-menu">
                            <li parentid={obj.parent}>
                                <Link
                                to={obj.url}>{obj.rubrik}</Link>
                            </li>
                        </ul>
                        )}
                    </li>
                </ul>
        )
    })
    return (
        <div className='sidebar'>
            {ort}
        </div>
    )
}

export default Nav

And here is the css

*{
    margin:0;
    padding:0;
    box-sizing: border-box;
    font-family: 'Courier New', Courier, monospace;
}

.sidebar{
    position: relative;
    top:0;
    left:0;
    height: 100%;
    width:260px;
    background: #ea5526;
}
.arrow{
    cursor: pointer;
}
.sidebar .logo-details{
    height:60px;
    width: 100%;
    display: flex;
    align-items: center;
}

.sidebar .logo-details i{
    font-size: 30px;
    color:#fff;
}

.sidebar .logo-details .logo_name
{
    font-size: 30px;
    color:#fff;
    font-weight: 600;
}

.sidebar .nav-links{
    background: #ea5526;
    height: 100%;
    padding-top: 30px;
}

.sidebar .nav-links li{
    position: relative;
    list-style: none;
    transition: all 0.4s ease;
}

.sidebar .nav-links li:hover{
    background: #f9712d;
}

.sidebar .nav-links li i{
    height: 50px;
    min-width: 78px;
    text-align: center;
    line-height: 50px;
    color:#fff;
}

.sidebar .nav-links li a{
    display: flex;
    align-items: center;
    text-decoration: none;
}

.sidebar .nav-links li a .link_name{
    font-size: 18px;
    font-weight: 400;
    color: #fff;
}


.sidebar .nav-links li .sub-menu{
    padding: 6px 6px 14px 80px;
    margin-top: -10px;
    background-color: #f9712d;
}

.sidebar .nav-links li.showMenu .sub-menu{
    display:block;
}
.sidebar .nav-links li .sub-menu a{
    color:#fff;
    font-size:15px;
    padding: 5px 0;
}

.sidebar.close .nav-links li .sub-menu{
    position: absolute;
    left:100%;
    top: 0;
    margin-top: 0;
    padding: 10px 20px;
}
1 Answers

You are iterating the overall flat structure of orter array, so no links are nested under a parent link.

Create a function to process the flat array structure an create a tree of links.

const createLinks = (links) =>
  Object.values(
    links.reduce((acc, el) => {
      if (el.parent === "1") {
        acc[el.id] = {
          ...el,
          children: []
        };
      } else {
        acc[el.parent].children.push(el);
      }
      return acc;
    }, {})
  );

The nested links are pushed into a children property.

Update the toggle state and logic so the UI isn't toggling all the submenus open together. The following stores the link id of a menu that has been toggled open. The toggleSubmenu handler allows the submenu to be opened without allowing the parent navigation to occur.

const [toggle, setToggle] = useState({});

const toggleSubmenu = (id) => (e) => {
  e.preventDefault();

  setToggle((ids) => ({
    ...ids,
    [id]: !ids[id]
  }));
};

Next is to update the render to map these children links.

const ort = createLinks(orter).map((obj) => {
  return (
    <ul className="nav-links">
      <li parentid={obj.parent} key={obj.id}>
        <Link to={obj.url}>
          <i className="knapp">#</i>
          <span className="link_name">{obj.rubrik}</span>{" "}
          <i className="arrow" onClick={toggleSubmenu(obj.id)}>
            [sub]
          </i>
        </Link>
        {toggle[obj.id] && ( // <-- check if id is toggled true
          <ul className="sub-menu">
            {obj.children.map((nestedObj) => ( // <-- render children links
              <li key={nestedObj.id}>
                <Link to={nestedObj.url}>{nestedObj.rubrik}</Link>
              </li>
            ))}
          </ul>
        )}
      </li>
    </ul>
  );
});

return <div className="sidebar">{ort}</div>;

Edit react-submenus-from-an-array

enter image description here

Related