Show/Hide children in menu when clicking on parent (with pure javascript)

Viewed 807

I have bare basic knowledge in JavaScript, and I'm trying to create a menu where :

  • Only the first level parents are visible.
  • Then when you click on them, their children will be displayed.
  • If you click on them again (or maybe click anywhere on the page) they will close
  • Must work for 2 level children (for example children of children)

This is what I have so far, and I know this is far from being right. I found a bunch of jQuery solutions online, but I want it pure JavaScript & CSS.

Note: I also need to keep the number of classes & id's used to a minimum as I want to be able to use this with the WordPress menu structure. But if I need to manually add a "parent" class to each parent that has children, then that will be fine.

My JavaScript :

var menuParents = document.querySelectorAll("#my-menu .parent");
menuParents.forEach(menuParent => {
   document.querySelector("#my-menu .parent").addEventListener("click", ToggleMenu);
      function ToggleMenu() {
         document.querySelector("#my-menu .parent ul").style.display = 'block';
      }
});

My CSS :

#my-menu .parent ul {display: none;}

And, finally, my HTML :

<ul id="my-menu">
   <li>Standard Item</li>
   <li>Standard Item</li>
   <li class="parent">Item with children
      <ul>
         <li>child item</li>
         <li>child item</li>
         <li class="parent">child item with children
           <li>second level child</li>
           <li>second level child</li>
         </li>
         <li>child item</li>
         <li>child item</li>
      </ul>
   </li>
   <li>Standard Item</li>
   <li>Standard Item</li>
   <li class="parent">Item with children
      <ul>
        <li>child item</li>
        <li>child item</li>
        <li class="parent">child item with children
           <li>second level child</li>
           <li>second level child</li>
        </li>
        <li>child item</li>
        <li>child item</li>
      </ul>
   </li>
   <li>Standard Item</li>
   <li>Standard Item</li>
</ul>
2 Answers

You need to use the specific element in the forEach, and then just get the firstElementChild of it, and toggle the display style:

const toggle = {
    block: 'none',
    none: 'block'
}

function ToggleMenu(event) {
    event.stopPropagation();
    event.target.classList.toggle('active');
    event.target.firstElementChild.style.display = toggle[event.target.firstElementChild.style.display] || 'block';
}
var menuParents = document.querySelectorAll("#my-menu .parent");
menuParents.forEach(menuParent => {
    menuParent.addEventListener("click", ToggleMenu);
})
#my-menu .parent ul {display: none;}
.active {
    color: red
}

.active > * {
    color: black
}
<ul id="my-menu">
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li class="parent">Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li class="parent">child item with children
              <ul>
                <li>second level child</li>
                <li>second level child</li>
              </ul>
            </li>
            <li>child item</li>
            <li>child item</li>
        </ul>
    </li>
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li class="parent">Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li class="parent">child item with children
              <ul>
                <li>second level child</li>
                <li>second level child</li>
              </ul>
            </li>
            <li>child item</li>
            <li>child item</li>
        </ul>
    </li>
    <li>Standard Item</li>
    <li>Standard Item</li>
</ul>

or without "parent" class

const toggle = {
    block: 'none',
    none: 'block'
}

function ToggleMenu(event) {
    event.stopPropagation();
    if (event.target.firstElementChild) {
      event.target.classList.toggle('active');
      event.target.firstElementChild.style.display = toggle[event.target.firstElementChild.style.display] || 'block';
    }
}
var menuParents = document.querySelectorAll("#my-menu li > ul");
menuParents.forEach(menuParent => {
    menuParent.parentElement.addEventListener("click", ToggleMenu);
})
#my-menu li > ul {display: none;}
.active {
    color: red
}

.active > * {
    color: black
}
<ul id="my-menu">
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li>Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li>child item with children
              <ul>
                <li>second level child</li>
                <li>second level child</li>
              </ul>
            </li>
            <li>child item</li>
            <li>child item</li>
        </ul>
    </li>
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li>Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li>child item with children
              <ul>
                <li>second level child</li>
                <li>second level child</li>
              </ul>
            </li>
            <li>child item</li>
            <li>child item</li>
        </ul>
    </li>
    <li>Standard Item</li>
    <li>Standard Item</li>
</ul>

Your HTML is slightly wrong: your parents inside parents don't have ul elements.

<ul id="my-menu">
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li class="parent">Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li class="parent">child item with children
                <!-- here -->
                <ul>
                    <li>second level child</li>
                    <li>second level child</li>
                </ul>
            </li>
        </ul>
    </li>
    <li>child item</li>
    <li>child item</li>
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li class="parent">Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li class="parent">child item with children
                <!-- and here -->
                <ul>
                    <li>second level child</li>
                    <li>second level child</li>
                </ul>
            </li>
        </ul>
    </li>
    <li>child item</li>
    <li>child item</li>
    <li>Standard Item</li>
    <li>Standard Item</li>
</ul>

JavaScript wise, your toggle function doesn't actually toggle anything, or it won't toggle more than once, anyway.

Another problem is that the click handler will also get called when you click on a parent of a parent, which this code accounts for too.

Here I used an anonymous function. You don't have to do this, a named function would work just fine, but it doesn't clutter your only namespace.

This will also work with any level of parents: the attached snippet has second level parents.

var menuParents = document.querySelectorAll("#my-menu .parent");
menuParents.forEach(
    menuParent => {
        menuParent.addEventListener(
            "click",
            // inline, anonymous function
            //  also accepts an event parameter
            function (event) {
                // check if the parent element was actually clicked (prevent parent of parent from toggling display)
                if (event.target == this) {
                    // this == menuParent
                    // select ul from menuParent
                    var menuChild = this.querySelector("ul");

                    // toggle child menu display, based on it's current display
                    if (menuChild.style.display == "block") {
                        // here, you could also set the display to null, since your CSS sets the display to none anyway
                        menuChild.style.display = "none";
                    } else {
                        menuChild.style.display = "block";
                    }
                }
            }
        );
    }
);

Tested on Chrome 87.0.4280.88.

var menuParents = document.querySelectorAll("#my-menu .parent");
menuParents.forEach(
    menuParent => {
        menuParent.addEventListener(
            "click",
            // inline, anonymous function
            //  also accepts an event parameter
            function (event) {
                if (event.target == this) {
                    // this == menuParent
                    // select ul from menuParent
                    var menuChild = this.querySelector("ul");

                    // toggle child menu display, based on it's current display
                    if (menuChild.style.display == "block") {
                        // here, you could also set the display to null, since your CSS sets the display to none anyway
                        menuChild.style.display = "none";
                    } else {
                        menuChild.style.display = "block";
                    }
                }
            }
        );
    }
);
#my-menu .parent ul {
    padding-left: 16px;
    display: none;
}
<ul id="my-menu">
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li class="parent">Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li class="parent">child item with children
                <ul>
                    <li>second level child</li>
                    <li>second level child</li>
                    <li class="parent">second level parent
                    <ul>
                        <li>third level child</li>
                        <li>third level child</li>
                    </ul>
                    <li>second level child</li>
                    <li>second level child</li>
                </ul>
            </li>
            <li>child item</li>
            <li>child item</li>
        </ul>
    </li>
    </li>
    <li>Standard Item</li>
    <li>Standard Item</li>
    <li class="parent">Item with children
        <ul>
            <li>child item</li>
            <li>child item</li>
            <li class="parent">child item with children
                <ul>
                    <li>second level child</li>
                    <li>second level child</li>
                    <li class="parent">second level parent
                    <ul>
                        <li>third level child</li>
                        <li>third level child</li>
                    </ul>
                    </li>
                    <li>second level child</li>
                    <li>second level child</li>
                </ul>
            </li>
            <li>child item</li>
            <li>child item</li>
        </ul>
    </li>
    <li>Standard Item</li>
    <li>Standard Item</li>
</ul>

Related