25

Let's presume you got a list with nested child lists.

<ul>
    <li></li>
    <li>
        <ul>
            <li></li>
            <li></li>
        </ul>
    </li>
    <li></li>
</ul>

And use document.querySelectorAll() to make a selection:

var ul = document.querySelectorAll("ul");

How can i use the ul collection to get the direct child elements?

ul.querySelectorAll("> li"); 
// Gives 'Error: An invalid or illegal string was specified'

Let's presume ul is cached somehow (otherwise i could have done ul > li directly).

In jQuery this works:

$("ul").find("> li");

But it doesn't in native querySelectorAll. Any solutions?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Husky
  • 5,757
  • 2
  • 46
  • 41
  • 3
    That selector doesn't really make sense. With jQuery you'd use `.children("li")` instead of `.find("> li")`. I'm surprised that works at all, actually. – Matt Ball Apr 17 '12 at 11:03
  • 1
    @MДΓΓБДLL `.find('> li ul span')` is useful, but not so much on its own like in the OP's example. – alex Apr 17 '12 at 11:09
  • @MДΓΓБДLL the example is a bit contrived, but it definitely has a real-world use case :) – Husky Apr 17 '12 at 11:14

3 Answers3

41

The correct way to write a selector that is "rooted" to the current element is to use :scope.

ul.querySelectorAll(":scope > li");

See my answer here for an explanation and a robust, cross-browser solution: https://stackoverflow.com/a/21126966/1170723

Community
  • 1
  • 1
lazd
  • 4,468
  • 3
  • 23
  • 17
  • 3
    Good to see this exists! :) – alex Oct 09 '14 at 23:07
  • 8
    Keep in mind the [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/%3ascope#Browser_compatibility): Chrome: yes, FF: 32, IE: no, opera: no, Safari: 7 – Web_Designer Jan 06 '15 at 22:55
  • 1
    in addition to what @Web_Designer said, to this day it is *still* also not supported in Microsoft Edge. Trying to use 'scope' in querySelectorAll gives a syntax error. So much for the "you don't need jQuery" camp. – TKoL Aug 30 '17 at 08:05
13

Because the ul returned is a NodeList, it doesn't implicitly loop over its contents like a jQuery collection. You'd need to use ul[0].querySelectorAll() or better still select the ul with querySelector().

Besides that, querySelectorAll() won't take a > and work from its current context. However, you can get it to work using lazd's answer (though check the browser compatibility), or any of these workarounds (which should have no browser issues)...

[].filter.call(ul.querySelectorAll("li"), function(element){
     return element.parentNode == ul;
}); 

jsFiddle.

This will select all li elements that are descendants of your ul, and then remove the ones which are not direct descendants.

Alternatively, you could get all childNodes and then filter them...

[].filter.call(ul.childNodes, function(node) {
    return node.nodeType == 1 && node.tagName.toLowerCase() == 'li';
});

jsFiddle.

Community
  • 1
  • 1
alex
  • 479,566
  • 201
  • 878
  • 984
  • Yup, that seems like a workable solution. Too bad it takes three lines of code what could be done in a single character :) Note that `filter()` won't work because `ul` is a NodeList, so you need to do `Array.prototype.slice.call` first. – Husky Apr 17 '12 at 11:04
  • @Husky You can always add it to `Element.prototype` as a function :) – Ja͢ck Apr 11 '13 at 06:15
2

You need to iterate over the NodeList returned by document.querySelectorAll() and then call element.querySelectorAll() for each element in that list.

Alnitak
  • 334,560
  • 70
  • 407
  • 495