Challenges and limitations with advanced selectors and the document.querySelectorAll() method
Yesterday, I wrote about how to get all direct descendants that match a test condition.
A substantial number of folks wrote to me asking why I would use the Array.filter()
method with the Node.children
property instead of using a nested selector with the document.querySelectorAll()
method.
let tuna = document.querySelectorAll('#sandwiches > .tuna');
This absolutely works when…
- The parent element has a unique selector, and
- You want to filter child elements based on a CSS selector.
If the parent element doesn’t have a unique selector (as in, no ID), you can use the :scope
pseudo-class (thanks to read Stefan for sharing this one with me!).
Here, :scope
refers to the sandwiches
element.
// This is silly, since the element obviously has an ID
// Just roll with it
let sandwiches = document.querySelector('#sandwiches');
// Here, :scope refers to the sandwiches element
let tuna = sandwiches.querySelectorAll(':scope > .tuna');
When you’re filtering with things that can be targeted with CSS, this works great. But what if your criteria is more elaborate?
For example, what if you wanted to exclude…
- The third child element
- Child elements with more than one nested element inside it
- Child elements with a nested button
In those situations, the Array.from(NodeChildren).fitler()
approach is, in my opinion, more robust, simpler, and in some cases, the only option.
// exclude the third child element
let noThirdChild = Array.from(sandwiches.children).filter(function (elem, index) {
return elem.matches('.tuna') && index !== 2;
});
// exclude more than one nested elements
let noNestedElements = Array.from(sandwiches.children).filter(function (elem) {
return elem.matches('.tuna') && elem.children.length < 2;
});
// exclude items with nested buttons
let noNestedButtons = Array.from(sandwiches.children).filter(function (elem) {
return elem.matches('.tuna') && !elem.querySelector('button');
});