34

As my understanding, when using element.querySelector(), the query should be start on particular element.

However, when I run using code below, it keep selected the first DIV tag in particular element.

const rootDiv = document.getElementById('test');
console.log(rootDiv.querySelector('div').innerHTML);
console.log(rootDiv.querySelector('div > div').innerHTML);
console.log(rootDiv.querySelector('div > div > div').innerHTML);
console.log(rootDiv.querySelector('div > div > div > div').innerHTML);
console.log(rootDiv.querySelector('div > div > div > div > div').innerHTML);
<div>
  <div>
    <div id="test">
      <div>
        <div>
        This is content
        </div>
      </div>
    </div>
  </div>
</div>

As you can see, the first few results is the same. This this a bug? Or it will query from start of the document?

Abana Clara
  • 4,602
  • 3
  • 18
  • 31
Joey Chong
  • 1,470
  • 15
  • 20

3 Answers3

28

What querySelector does is it finds an element somewhere in the document that matches the CSS selector passed, and then checks that the found element is a descendant of the element you called querySelector on. It doesn't start at the element it was called on and search downwards - rather, it always starts at the document level, looks for elements that match the selector, and checks that the element is also a descendant of the calling context element. It's a bit unintuitive.

So:

someElement.querySelector(selectorStr)

is like

[...document.querySelectorAll(selectorStr)]
  .find(elm => someElement.contains(elm));

A possible solution is to use :scope to indicate that you want the selection to start at the rootDiv, rather than at document:

const rootDiv = document.getElementById('test');
console.log(rootDiv.querySelector(':scope > div').innerHTML);
console.log(rootDiv.querySelector(':scope > div > div').innerHTML);
console.log(rootDiv.querySelector(':scope > div > div > div').innerHTML);
<div>
  <div>
    <div id="test">
      <div>
        <div>
        This is content
        </div>
      </div>
    </div>
  </div>
</div>

:scope is supported in all modern browsers but Edge.

Nisarg Shah
  • 14,151
  • 6
  • 34
  • 55
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • `:scope` is not supported by all but most of the major browsers. Anyways, thank for explanation and this should be the answer. Thanks. – Joey Chong Apr 08 '19 at 07:44
  • `:scope` doesn't work with IE and Edge, I think it's important to point this out – Christian Vincenzo Traina Apr 08 '19 at 07:47
  • [Here](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope#Browser_compatibility) is the browser compatibility table - we have to resist just few months more since Edge is implementing Chromium engine :P – Christian Vincenzo Traina Apr 08 '19 at 07:47
  • Do you have any sources for the browser engine actually working this way ? - The query would have the same result, if the engine would start with the first div and check if it matches the query selector... – Falco Apr 08 '19 at 11:17
7

The currently accepted answer somehow provides a valid logical explanation as to what happens, but they are factually wrong.

Element.querySelector triggers the match a selector against tree algorithm, which goes from the root element and checks if its descendants do match the selector.

The selector itself is absolute, it doesn't have any knowledge of a Document and doesn't even require that your Element be appended to any. And apart from the :scope attribute, it doesn't either care with which root you called the querySelector method.

If we wanted to rewrite it ourselves, it would be more like

const walker = document.createTreeWalker(element, {
  NodeFilter.SHOW_ELEMENT,
  { acceptNode: (node) => return node.matches(selector) && NodeFilter.FILTER_ACCEPT }
});
return walker.nextNode();

const rootDiv = document.getElementById('test');
console.log(querySelector(rootDiv, 'div>div').innerHTML);

function querySelector(element, selector) {
  const walker = document.createTreeWalker(element, 
    NodeFilter.SHOW_ELEMENT,
    {
      acceptNode: (node) => node.matches(selector) && NodeFilter.FILTER_ACCEPT
    });
  return walker.nextNode();
};
<div>
  <div>
    <div id="test">
      <div>
        <div>
          This is content
        </div>
      </div>
    </div>
  </div>
</div>

With the big difference that this implementation doesn't support the special :scope selector.

You may think it's the same going from the document or going from the root element, but not only will it make a difference in terms of performances, it will also allow for using this method while the element is not appended to any document.

const div = document.createElement('div');
div.insertAdjacentHTML('beforeend', '<div id="test"><div class="bar"></div></div>')

console.log(div.querySelector('div>.bar')); // found
console.log(document.querySelector('div>.bar')); // null

In the same way, matching elements in the Shadow-DOM would not be possible if we only had Document.querySelector.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Goood explanaition - especially the bit about Nodes not part of the document. This represents more clearly what the browser engine actually does. – Falco Apr 08 '19 at 13:46
1

The query selector div > div > div only means:

Find a div which has a parent and a granparent which are both also a div.

And if you start with the first child of test and check the selector, it is true. And this is the reason why only your last query selects the innermost div, since it has the first predicate (find a div with a great-great-grandparent-div) which is not fulfilled by the first child of test.

The query-selector will only test descendants, but it will evaluate the expression in scope of the whole document. Just imagine a selector like checking properties of an element - even if you only view the child element, it is still the child of its parent.

Falco
  • 3,287
  • 23
  • 26