0

I am trying to create a multilayer or nested accordion menu using pure JavaScript. The whole thing is functioning correctly expect for one issue: when I click on one element of the menu, the entire thing reacts.

This is not for a current project. I am simply trying to expand my knowledge after taking a basic course.

I know the easy-but-bloated way to fix this would be to replicate the functions three times and then specifically target each node (is that even the right word?) in the array. However, I image that would be the wrong way to do things. As you’ll see, I attempted to create a loop for multiple variables. I think this might be my issue because it seems to be the thing that is linking all of them together.

var accordion = document.getElementsByClassName("accordion");
var dropdown = document.getElementsByClassName("dropdown");
var accordionArrow = document.getElementsByClassName("accordionArrow");
var dropdownArrow = document.getElementsByClassName("dropdownArrow");
var content = document.getElementsByClassName("content");

function accordionFunction() {
  for (j = 0, k = 0, l = 0, m = 0; j < dropdown.length, k < accordionArrow.length, l < dropdownArrow.length, m < content.length; j++, k++, l++, m++) {
    if (dropdown[j].style.maxHeight) {
      dropdown[j].style.maxHeight = null;
      accordionArrow[k].style.transform = null;
      content[m].style.maxHeight = null;
      dropdownArrow[l].style.transform = null;
    } else {
      dropdown[j].style.maxHeight = dropdown[j].scrollHeight + "px";
      accordionArrow[k].style.transform = "rotate(-135deg)";
    }
  }
};

for (var i = 0; i < accordion.length; i++) {
  accordion[i].addEventListener("click", accordionFunction);
};

function accordionSubmenu() {
  for (l = 0, m = 0; l < dropdown.length, m < content.length; l++, m++) {
    if (content[m].style.maxHeight) {
      content[m].style.maxHeight = null;
      dropdownArrow[l].style.transform = null;
    } else {
      content[m].style.maxHeight = content[m].scrollHeight + "px";
      dropdownArrow[l].style.transform = "rotate(-135deg)";
    }
  }
};

for (j = 0; j < dropdown.length; j++) {
  dropdown[j].addEventListener("click", accordionSubmenu)
};
body {
  margin: auto;
  width: 600px;
}

div {
  margin: auto;
}

.accordion {
  background-color: lightblue;
  color: white;
  padding: 3%;
  cursor: pointer;
  width: 300px;
  height: 50px;
}

.accordion .accordionArrow {
  border: solid white;
  border-width: 0 3px 3px 0;
  display: inline-block;
  padding: 3px;
  transform: rotate(45deg);
}

.dropdown {
  color: lightblue;
  padding-left: 3%;
  cursor: pointer;
  width: 300px;
  max-height: 0;
  overflow: hidden;
  transition-duration: 0.2s;
}

.dropdown .dropdownArrow {
  border: solid lightblue;
  border-width: 0 3px 3px 0;
  display: inline-block;
  padding: 3px;
  transform: rotate(45deg);
}

.content {
  font-weight: bold;
  transition: all 0.2s ease;
  padding-left: 5%;
  max-height: 0;
  overflow: hidden;
}
<div>
  <h2 class="accordion">Main 1<i class="accordionArrow"></i></h2>
  <h3 class="dropdown">Submenu 1<i class="dropdownArrow"></i></h3>
  <p class="content">Hello there. We are exposed.</p>
</div>
<div>
  <h2 class="accordion">Main 2<i class="accordionArrow"></i></h2>
  <h3 class="dropdown">Submenu 1<i class="dropdownArrow"></i></h3>
  <p class="content">Hello there. We are exposed again!</p>
</div>
<div>
  <h2 class="accordion">Main 3<i class="accordionArrow"></i></h2>
  <h3 class="dropdown">Submenu 1<i class="dropdownArrow"></i></h3>
  <p class="content">Hello there. We are exposed thrice!</p>
</div>
Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
rguttersohn
  • 464
  • 5
  • 15
  • You’re binding each event listener separately, but you don’t distinguish which menu you clicked on. Try using the [event argument](https://stackoverflow.com/q/35936365/4642212) or [`this`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_value_of_this_within_the_handler), or use [event delegation](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_delegation) instead. – Sebastian Simon Oct 09 '19 at 13:57
  • You’re using `j`, `k`, `l` and `m` in your `for` loop… In the condition part `j < dropdown.length, k < accordionArrow.length, l < dropdownArrow.length, m < content.length`, [everything is ignored](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_Operator) except `m < content.length`. Shouldn’t you be using some [logical operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators)? – Sebastian Simon Oct 09 '19 at 14:01
  • Thanks for the prompt replies. Let me look into these sources and see if one of them solves this issue. – rguttersohn Oct 09 '19 at 14:12

1 Answers1

0

With some smart use of the this keyword (the element that triggered the event handler) and the nextElementSibling property you can achieve this in a more elegant way.

I made a class to make elements max-height 0. So now i can hide and show elements by just adding or removing this class.

In the accordion function I toggle this class for the element after the clicked element and remove the class for all others.

In the accordion submenu I just toggle the class on the nextElementSibling.

Personally I would make a different html structure where you can toggle classes easier by using more levels of elements so you would only need 1 function for x amount of submenu's. Maybe a nice new challenge for you.

document.querySelectorAll('.accordion').forEach((accordion) => accordion.addEventListener('click', function() {
  //Get All possible hidden elements and loop over it.
  document.querySelectorAll('.dropdown, .content').forEach((collapsible) => {
    //If current element is the same is the next sibling element of the event target toggle the class. Otherwise add it.
    if(collapsible === this.nextElementSibling) {
      collapsible.classList.toggle('maxHeightZero');
    }
    else {
      collapsible.classList.add('maxHeightZero');
    }
  });
}));

document.querySelectorAll('.dropdown').forEach((dropdown) => dropdown.addEventListener('click', function() {
  //toggle the nextElementSibling
  this.nextElementSibling.classList.toggle('maxHeightZero');
}));
body {
  margin: auto;
  width: 600px;
}

div {
  margin: auto;
}

.accordion {
  background-color: lightblue;
  color: white;
  padding: 3%;
  cursor: pointer;
  width: 300px;
  height: 50px;
}

.accordion .accordionArrow {
  border: solid white;
  border-width: 0 3px 3px 0;
  display: inline-block;
  padding: 3px;
  transform: rotate(45deg);
}

.dropdown {
  color: lightblue;
  padding-left: 3%;
  cursor: pointer;
  width: 300px;
  overflow: hidden;
  transition-duration: 0.2s;
}

.dropdown .dropdownArrow {
  border: solid lightblue;
  border-width: 0 3px 3px 0;
  display: inline-block;
  padding: 3px;
  transform: rotate(45deg);
}

.content {
  font-weight: bold;
  transition: all 0.2s ease;
  padding-left: 5%;
  overflow: hidden;
}

.maxHeightZero { 
  max-height: 0;
}
<div>
  <h2 class="accordion">Main 1<i class="accordionArrow"></i></h2>
  <h3 class="dropdown maxHeightZero">Submenu 1<i class="dropdownArrow"></i></h3>
  <p class="content maxHeightZero">Hello there. We are exposed.</p>
</div>
<div>
  <h2 class="accordion">Main 2<i class="accordionArrow"></i></h2>
  <h3 class="dropdown maxHeightZero">Submenu 1<i class="dropdownArrow"></i></h3>
  <p class="content maxHeightZero">Hello there. We are exposed again!</p>
</div>
<div>
  <h2 class="accordion">Main 3<i class="accordionArrow"></i></h2>
  <h3 class="dropdown maxHeightZero">Submenu 1<i class="dropdownArrow"></i></h3>
  <p class="content maxHeightZero">Hello there. We are exposed thrice!</p>
</div>
Mark Baijens
  • 13,028
  • 11
  • 47
  • 73
  • Thank you so much! Yes, I made my CSS and HTML over-complicated only because I kept adding classes to see if it would fix the issue. – rguttersohn Oct 09 '19 at 14:29
  • @rguttersohn Your welcome, updated the code a bit to remove global variables and named functions. Also changed the old fashioned getElementsByClassName function to the new QuerySelectorAll. If you have any questions regarding this code feel free to ask. If this fully answers your question then please accept it (checkmark top left of the question) so people can see you don't need help anymore. – Mark Baijens Oct 09 '19 at 14:33
  • I still don't fully understand when to use querySelector vs getElementsByClassName. If you know of a clear explanation, that would be wonderful. – rguttersohn Oct 09 '19 at 18:29
  • @rguttersohn `querySelector()` uses css selectors and returns 1 node. `querySelectorAll()` uses css selectors and returns a node list. This way you can target elements the same way as in css. I prefer that over `getElementsByClassName()`, `getElementsByName()` and `getElementById()` all the time unless you care about the tiny performance loss. – Mark Baijens Oct 09 '19 at 18:42