'How to get all siblings of a parent item with specific classes in Vanilla JS
I have a long list of list items (generated via an API) and I would like to show/hide some of the items with click, something like an accordion effect.
The list items that I would like to hide by default have P-item class, and the M-item class above them is what it should be triggering them to show.
Here is my markup:
<li class="session-item M-item"></li>
<li class="session-item P-item"></li>
<li class="session-item P-item"></li>
<li class="session-item G-item"></li>
P-items are children of their parent M-item. So by default in the above example, the two P-items should be hidden and M-item and G-item should be showing. When M-item is clicked, then the two hidden P-items show. I am only listing 4 items in the above example but the list goes on and the only separator between this relationship is the G-items.
I got it to work but it's only showing/hiding the first direct sibling of M-item, I used nextElementSibling, like this:
var acc = document.getElementsByClassName("M-item");
var i;
for (i = 0; i < acc.length; i++) {
acc[i].addEventListener("click", function () {
this.classList.toggle("active");
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
}
What's the best way to select all siblings with the P-item and then break, looks for another M-item and repeat?
Is there something similar to nextAll(); in Vanilla JS? Do I need a while loop to iterate over the next sibling?
Here's how my full markup looks like:
Solution 1:[1]
I think the OP aims to find adjacent siblings with the same class.
One idea would be to intervene a little earlier, before the li's are setup as siblings, and impose an html structure there, grouping the like-classed siblings in div's (or ul's). But, if we're starting from the already built list, it can be handled as a reduce on the collection of li's...
const sessionItems = document.getElementsByClassName("session-item");
let groups = Array.from(sessionItems).reduce((acc, el, index) => {
const pItem = el.classList.contains("p-item")
const delimeter = !pItem || index===0;
if (delimeter) acc.push([]);
if (pItem) acc[acc.length-1].push(el);
return acc
}, []);
// now do whatever we want to the groups, like change visibility
// for clarity in this example, add a color class
groups.forEach((group, index) => {
group.forEach(el => el.classList.add(`color-${index}`))
});
.color-0 {
color: red;
}
.color-1 {
color: green;
}
<ul>
<li class="session-item m-item">item A</li>
<li class="session-item p-item">item B</li>
<li class="session-item p-item">item C</li>
<li class="session-item m-item">item D</li>
<li class="session-item p-item">item E</li>
<li class="session-item p-item">item F</li>
<li class="session-item p-item">item G</li>
<li class="session-item m-item">item H</li>
</ul>
Solution 2:[2]
You can maintain the visibility of the P items through CSS only, based on the "active" that the preceding M item has. This means the JS code only has to manage the toggling of the "active" class on the M item:
const ul = document.querySelector(".cad-agenda");
ul.addEventListener("click", function (e) {
const mItem = e.target;
if (!mItem.classList.contains("M-item")) return;
const wasActive = mItem.classList.contains("active");
document.querySelector(".active")?.classList?.remove("active");
mItem.classList.toggle("active", !wasActive);
});
.P-item { display: none }
.active ~ .P-item:not(.active ~ .M-item ~ .P-item) { display: block }
.M-item { cursor: pointer; font-weight: bold }
<ul class="cad-agenda accordion accordion-1">
<li class="session-item M-item">M-1</li>
<li class="session-item P-item">P-1.1</li>
<li class="session-item P-item">P-1.2</li>
<li class="session-item G-item">G-1</li>
<li class="session-item M-item">M-2</li>
<li class="session-item P-item">P-2.1</li>
<li class="session-item P-item">P-2.2</li>
<li class="session-item M-item">M-3</li>
<li class="session-item P-item">P-3.1</li>
<li class="session-item P-item">P-3.2</li>
<li class="session-item Press-item">PressItem</li>
<li class="session-item M-item">M-3</li>
<li class="session-item P-item">P-3.1</li>
<li class="session-item M-item">M-4</li>
<li class="session-item P-item">P-4.1</li>
<li class="session-item M-item">M-5</li>
<li class="session-item P-item">P-5.1</li>
<li class="session-item G-item">G-2</li>
<li class="session-item M-item">M-6 (has no P)</li>
<li class="session-item -item">empty 1</li>
<li class="session-item -item">empty 2</li>
<li class="session-item -item">empty 3</li>
<li class="session-item M-item">M-7 (has no P)</li>
<li class="session-item M-item">M-8</li>
<li class="session-item P-item">P-8.1</li>
<li class="session-item P-item">P-8.2</li>
</ul>
Solution 3:[3]
From css set display: none; of all elements with class P-item
An event listener is placed on all items with the M-item class.
When an item with class M-item is clicked, the function myFunction() is called.
Step 1: Removes all active classes.
Step 2: Loop through all the items until it reaches the clicked item. When it reaches the clicked element of the variable flag, the value is set to true. The active class is added to the clicked element and to all subsequent elements with class P-item. The loop breaks when it reaches an element with a different class from P-item
const wrap = document.querySelector('.wrap');
const listAll = wrap.querySelectorAll('li');
const listM = wrap.querySelectorAll('.M-item');
listM.forEach(el => {
el.addEventListener('click', myFunction)
});
function myFunction() {
listAll.forEach(el => {
el.classList.remove('active')
});
let flag = false;
for (let el of listAll) {
if (el === this) {
flag = true;
}
if (flag) {
if (el === this || el.classList.contains('P-item')) {
el.classList.add('active');
} else {
break;
}
}
}
};
.M-item {
cursor: pointer;
}
.P-item {
display: none;
}
.active {
display: block;
}
<ul class="wrap">
<li class="session-item M-item">M-item Click me</li>
<li class="session-item P-item">P-item</li>
<li class="session-item P-item">P-item</li>
<li class="session-item G-item">G-item</li>
<li class="session-item M-item">M-item Click me</li>
<li class="session-item P-item">P-item</li>
<li class="session-item P-item">P-item</li>
<li class="session-item G-item">G-item</li>
</ul>
Solution 4:[4]
Late arrival. I've used a data attribute to link parent to child.
let count = 0;
document
querySelectorAll("[class~=session-item]")
.forEach((s)=>
{
if (s.classList.contains("M-item")) {
s.addEventListener("click", function () {
this.classList.toggle("active");
document
.querySelectorAll(`[data-id='C${this.getAttribute("data-id")}']`)
.forEach( (p) => p.classList.toggle("hide") );
});
s.setAttribute("data-id", ++count);
}
else
{
s.setAttribute("data-id", `C${count}`);
}
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | danh |
| Solution 2 | trincot |
| Solution 3 | |
| Solution 4 |

