querySelector Quirks 1

Unless you’re supporting IE7 or less, you should be using querySelector and querySelectorAll. You just should. caniuse.com says it’s okay. And there’s MDN documentation on the matter. Here are some links. Read them, and love everything about QS(A).

Good stuff, but there is one little oddity that a StackOverflow user named Greg encountered last week. It has to do with Element.querySelector, and how it interprets the selector passed as an argument. Consider the following HTML structure:

<ul class="menu">
    <li>
        <a href="#test">Menu Item</a>
        <ul class="sub-menu">
            <li><a href="#sub">Sub-menu Item</a></li>
            <li><a href="#sub">Sub-menu Item</a></li>
            <li><a href="#sub">Sub-menu Item</a></li>
            <li><a href="#sub">Sub-menu Item</a></li>
            <li><a href="#sub">Sub-menu Item</a></li>
            <li><a href="#sub">Sub-menu Item</a></li>
        </ul>
    </li>
    <li>...</li>
    <li>...</li>
    <li>...</li>
    <li>...</li>
</ul>

This is a pretty standard menu/submenu structure. Now, the strange part. What happens when you try to use QSA to get some elements?

var item = document.querySelector('.menu li:first-child'),
    subItem = item.querySelector('ul li a');

console.log(subItem.text); // "Menu Item"

Strange. Unexpected. Most likely, not what we wanted.

Here’s a JSBin to play with: http://jsbin.com/adicej/2/edit

What we’re expecting the engine to do is scope item.querySelector(‘ul li a’) to all elements within item (the first li within .menu). And it does… sort of. It turns out that it starts with all elements within item, and then checks to see if they match a CSS selector that’s scoped to the entire document.

See, querySelector[All] gets the element(s) that match a CSS selector, using the CSS engine of the browser. The CSS engine interprets the selectors from right to left, so in the case of our second call, it would go through the following steps:

  1. Find all a elements within item
  2. Filter this to all a elements that are within an li
  3. Filter this to all a elements that are within an li within a ul
  4. Return the matching elements as a non-live NodeList

So, if we see these three steps in terms of which elements exist in the set, we get the following sets:

Step 1: (all a elements within subItem)

<a href="#test">Menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>

Step 2: (all of those that are within an li – so, all of them)

<a href="#test">Menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>

Step 3: (all of those that are within a ul – so, again, all of them)

<a href="#test">Menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>
<a href="#sub">Sub-menu Item</a>

Step 4: querySelector returns the first element

Yes! All of these anchors match the selector! They’re all anchors within list items within unordered lists, and querySelector returns whichever is first.

I hope it becomes clear what has happened here. The selector passed to Element.querySelector[All] doesn’t “top out” at the element, but it will find only elements that are within that element. The selector functions exactly as it would in CSS, but it starts with children of the element on which it is called. All the parents of these elements – even those that are parents of (“beyond the scope of”) subItem – will be considered by the CSS engine.

To “correct” it, we have to change our selector to be more specific:

var item = document.querySelector('.menu li:first-child'),
    subItem = item.querySelector('.sub-menu li a');

console.log(subItem.text); // "Sub-menu Item"

Odd, but not incorrect (to use a double-negative). Or, at least, that’s my opinion. What do you think?

About Me

I wear several hats, and I do a lot of different things. By day, I’m a mild-mannered web developer for McKissock Education. By night, I’m CTO and Lead Developer of SlickText.com – Text Message Marketing service. I’m also a musician, songwriter, dancer, and pretty cool guy.

February 18th, 2013 by

One comment on “querySelector Quirks

  1. Reply DaveRandom Mar 22, 2013 7:57 am

    John Resig also touches on this oddity in this post: http://ejohn.org/blog/thoughts-on-queryselectorall/

    This is a really serious problem with QSA in my opinion. Literally every framework with a selector engine works in the exact same way for relative selectors: the selector starts at the base node, and nothing above it in the document tree is ever considered.

    There are two key reasons for this, firstly it would be even more expensive to execute that it currently is because it would involve (potentially) traversing the DOM right back up to the root node, but more importantly it is completely counter intuitive – selectors are in the respect similar to file system paths, if I specify a relative file system path I cannot go above the base node unless I do it explicitly with .. and CSS selectors should work the same way.

    Browsers are implementing the specification correctly by doing it this way, but the specification itself is wrong. This needs to be rectified as soon as possible, which either means a BC break or *another* new API.

    🙁

Leave a Reply