Skip to content
Advertisement

How to remove all classes from a menu except from the actual active element?

I have a menu which opens a submenu onclick by adding a class active to the corresponding element. However, whenever the first submenu was opened it stayed active if a second submenu was opened and so on.

Therefore I added a forEach to first remove all active classes and only after that add the active class to the next submenu.

Unfortunately, this causes the problem, I can’t wrap my head around: I need the active menu to close if the link is clicked again.

The following code does not work in this case, because when clicking the same link it will first remove all active classes and then add it again which causes the menu to stay open instead of closing.

const megamenu = document.querySelector('.megamenu');
const menuSection = megamenu.querySelector('.megamenu-section');  
const submenus = document.querySelectorAll('.megamenu-submenu');

menuSection.addEventListener('click', (e) => {
    e.preventDefault();
    submenus.forEach(submenu => {
        if (submenu.classList.contains('active')) {
            submenu.classList.remove('active');
            console.log("hasActive")
    })
    e.target.closest('.megamenu-submenu').classList.toggle('active');
});

I think I need a way to remove all active classes except for the actual active submenu.

Any way to achieve this? Or any better solution? Thanks.

Advertisement

Answer

The multiple approaches are for demonstration that there are always multiple ways to solve a single problem in programming.
Some might be more efficient, others might be more readable, etc.

You may chose whichever approach you prefer.

Approach 1 (recommended by me)

Save reference of last active submenu as lastActive.
onclick, remove .active from lastActive, and toggle .active on clicked submenu.

To fix the case where lastActive is the clicked submenu, we toggle depending on if .active was present or not before removal on lastActive.

This requires no looping, and almost no branching (if-statements) from our part (but most likely uses some in the native code (may use some anyway)).
But I think it creates a single closure (no problem).

So that lastActive is not visible in the global context or the rest of the script, we encapsulate it inside an IIFE.

const megamenu = document.querySelector('.megamenu');
const menuSection = megamenu.querySelector('.megamenu-section');  
const submenus = document.querySelectorAll('.megamenu-submenu');


(function() {
  let lastActive = submenus[0];
  menuSection.addEventListener('click', evt => {
    const currentSubmenu = evt.target.closest('.megamenu-submenu');
    if (!currentSubmenu) return;
    
    const wasActive = currentSubmenu.classList.contains('active');
    
    lastActive.classList.remove('active');
    currentSubmenu.classList.toggle('active', !wasActive);
    
    lastActive = currentSubmenu;
  });
})();
.megamenu-submenu {
  border: 1px solid black;
  height: 1.6rem;
  box-sizing: border-box;
}
.megamenu-submenu.active {
  background-color: rgba(255, 0, 0, .3);
}
<div class="megamenu">
  <div class="megamenu-section">
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
  </div>
</div>

Approach 2

Loop through each active submenu and remove .active, except for the currently clicked one. Toggle .active of the clicked one.

To only loop through the currently active submenus, we need to query inside the listener. This may hit the performance (minimally) for excessively many elements.

const megamenu = document.querySelector('.megamenu');
const menuSection = megamenu.querySelector('.megamenu-section');  
const submenus = document.querySelectorAll('.megamenu-submenu');

menuSection.addEventListener('click', evt => {
  const currentSubmenu = evt.target.closest('.megamenu-submenu');
  if (!currentSubmenu) return;
  
  for (const submenu of document.querySelectorAll('.megamenu-submenu.active')) {
    if (submenu !== currentSubmenu)
      submenu.classList.remove('active');
  }
  currentSubmenu.classList.toggle('active');
});
.megamenu-submenu {
  border: 1px solid black;
  height: 1.6rem;
  box-sizing: border-box;
}
.megamenu-submenu.active {
  background-color: rgba(255, 0, 0, .3);
}
<div class="megamenu">
  <div class="megamenu-section">
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
  </div>
</div>

Approach 3

Similar to approach 2, but loop through the already existing NodeList submenus.

This may hit the performance (minimally) for excessively many submenus, but should generally perform better than approach 2.

const megamenu = document.querySelector('.megamenu');
const menuSection = megamenu.querySelector('.megamenu-section');  
const submenus = document.querySelectorAll('.megamenu-submenu');

menuSection.addEventListener('click', evt => {
  const currentSubmenu = evt.target.closest('.megamenu-submenu');
  if (!currentSubmenu) return;
  
  for (const submenu of submenus) { // Only changed line
    if (submenu !== currentSubmenu)
      submenu.classList.remove('active');
  }
  currentSubmenu.classList.toggle('active');
});
.megamenu-submenu {
  border: 1px solid black;
  height: 1.6rem;
  box-sizing: border-box;
}
.megamenu-submenu.active {
  background-color: rgba(255, 0, 0, .3);
}
<div class="megamenu">
  <div class="megamenu-section">
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
    <div class="megamenu-submenu"></div>
  </div>
</div>
User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement