3

I have the following DOM structure:

    var parent = document.querySelector(".test");
    var navChild = parent.querySelectorAll("ul > li > a");
    for (i = 0; i < navChild.length; i++) {
            navChild[i].style.backgroundColor = "red";
        }
    console.log(navChild.lenght);
    console.log(navChild);
<!-- begin snippet: js hide: false console: true babel: false -->
    <ul>
      <li class="test">
        <a>ssss</a> <!--this will also be affected -->
        <ul>
          <li><a>fsfsf</a></li>
          <li><a>sff</a></li>
          <li><a>ggg</a></li>
        </ul>
      </li>
    </ul>

I want to select the 3 a tags using vanilla javascript and starting from the li with the class test. I put this in a parent variable and then use querySelectorAll to try to get only the a tags that are inside the next list. However, the nodelist that is returned also includes the very first a tag even though it's not a descendant. This is my code:

var navChild = parent.querySelectorAll("ul > li > a");

What's really weird is that if I query for just the li descendants I get only the 3. So I don't understand why when I query for the child descendant of that li I also get that first a tag that is two levels up on the DOM.

I know I could do this using jQuery but I don't want to use it.

EDITED to show how I'm setting the parent variable

I'm trying to add those a tags to the childs variable after I've hit the enter keyboard button while on the first a tag that I don't want to include in the childs node list.

var alinks = document.querySelectorAll('.test > a');
[].forEach.call(alinks, function(link){
    link.addEventListener('keyup', function(e){
        e.preventDefault();
        if(e.keyCode === 13) {
            var linkParent = e.target.parentElement;
            var childs = menuParent.querySelectorAll('ul > li > a');
        }        
    })
});

When I log linkParent it is correctly being set as the li with class test. I don't understand why then if I run the query from there it still includes the very first a tag. As I previously stated, if I query just ul > li it gives me the correct li tags.

chavab_1
  • 237
  • 2
  • 10

3 Answers3

3

This is because the first <a> tag you're trying to ignore still falls into the selector rule ul > li > a. So, even though you start the query with the <li class="test"> as the root (which does work by the way, I don't know why the other answers say that the document is still the root), the first child element it finds is the <a> tag and, indeed, it is the child of an <li> which is the child of a <ul>. The fact that this winds up going "above" your specified root is ignored.

Edit: If you want the selector rule to be scoped to the root as well, you can use this instead:

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

And to further clarify, browser CSS engines evaluate CSS rules right-to-left. In your mind, you want the browser to start at the root (the parent <li class="test">) and then find a <ul> tag and then find a <li> tag and then find an <a> tag.

However, the browser starts with the root, but then looks for all of the <a> tags below the root. Then it checks if the <a> tag is within an <li> and then if the <li> is within a <ul>. So the <li class="parent"> tag really is the root, but it does not follow the left-to-right hierarchy of your selector rule after that, it goes right-to-left.

skyline3000
  • 7,639
  • 2
  • 24
  • 33
  • So you're saying that when running that type of descendant query it always ignores the root and just finds items that fit into the selector rule?? – chavab_1 Mar 29 '18 at 19:46
  • @chavab_1 No, that's not what I'm saying. It will only find items that fit into the selector rule that are below the root -- that first `` tag is below the root. But, the part that it "ignores" is that the `` tag's parent elements which are what make it fit into the rule can be above the root. – skyline3000 Mar 29 '18 at 20:55
  • @chavab_1 If that's not the behavior you want, see my edit about adding in `:scope` – skyline3000 Mar 29 '18 at 21:00
  • Thank you very much. I didn't know that; I was expecting the same behavior as when the selector rule is used in CSS. I found the exact answer, just as you have explained, in this link: https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll Everything is working now. Once again, Thanx!! – chavab_1 Mar 29 '18 at 21:22
  • I found out that the `:scope` pseudo-class is not supported in IE (big surprise) nor Opera. Do you have an alternate way to tackle this task?? I could just disregard the first node in the nodelist and it would fix that issue, but then I also have to make sure I only select the nodes that are direct descendants since each of the child `a`s could also have a `ul` , `li` and `a`s of its own. The only thing I can think of is to use the `children` method until I get to the nodes that I need. But I just think there has to be a better solution. – chavab_1 Mar 29 '18 at 22:27
1

It's because your first <a> also matches ul > li > a, and since said a is still a descendant of the element qSA was called upon, it gets included in the NodeList.

https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll#Javascript

Element.querySelector and Element.querySelectorAll are somewhat quirky in that respect: although the only matching elements must be children of the Element you called it on, the selector string still starts at the document level - it doesn't verify the selector by isolating the element and its descendants first.

const child = document.querySelector('#child');
const span = child.querySelector('#parent span');
console.log(span.textContent);
<div id="parent">
<div id="child">
<span>text</span>
</div>
</div>
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • I think this doesn't really answer the question. the child selector (`>`) is being used, so it should only be returning `a` elements that are children of `li` elements that in turn are children of `ul` elements. This is acting more like he used `ul > li, li > a` – Tibrogargan Mar 28 '18 at 22:43
  • The `a` he wasn't expecting to be selected *is* an `a` that matches `ul > li > a`. – CertainPerformance Mar 28 '18 at 22:49
  • 2
    I wouldn't call it "quirky". It accurately selects elements that meet the description of the selector. They can use `:scope` to constrain the selection. `parent.querySelectorAll(":scope ul > li > a");` –  Mar 28 '18 at 23:18
0

Use this to define your var navChild = document.querySelectorAll("test > ul > li > a");. I had to use a running example to solve the issue. This will get the 3 vanilla <a> you want

The querySelectorAll() method returns all elements in the document that matches a specified CSS selector(s), as a static NodeList object.

I presume this is true even when you set a parent within the document.

var navChild = document.querySelectorAll(".test > ul > li > a");
for (i = 0; i < navChild.length; i++) {
        navChild[i].style.backgroundColor = "red";
}
//console.log(navChild.lenght);
//console.log(navChild);
<ul>
  <li class="test">
    <a>qqqq</a>
    <ul>
      <li><a>aaa</a></li>
      <li><a>bbb</a></li>
      <li><a>ccc</a></li>
    </ul>
  </li>
</ul>
mcv
  • 1,380
  • 3
  • 16
  • 41
  • In your fiddle, the `test` does not match `ul > li > a` because the `a` in question is not a direct descendant of a `li` - it's a descendant of a *span*. – CertainPerformance Mar 29 '18 at 22:26
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/167841/discussion-between-mcv-and-certainperformance). – mcv Mar 29 '18 at 22:57