Given the following markup (I've only included the relevant part for the sake of brevity)
<ul>
<li id="item">
<a href="#foo">Stuff</a>
<ul>
<li> <a href="#foo">Foo stuff</a></li>
<li><a href="#bar">Bar Stuff</a></li>
<li><a href="#foobar">Foo-Bar Stuff</a></li>
</ul>
</li>
</ul>
Let's say that I have a reference to the li element with the ID of 'item' which I could get like this:
let item = document.querySelector('#item');
Now I need to retrieve the last anchor element within the nested ul list. For this I'm going to use the 'last-child' selector. This is what I initially tried:
let lastLink = item.querySelector('li:last-child > a');
I thought this would do the trick, but alas it actually returns the very first link in the markup ('Stuff'). I don't quite understand this as my base-element is already the 'item' element and I'm asking it to find the li element within that base-element which is the last-child of its parent and then return the a element from within it.
Indeed, if I do this:
let lastLink = item.querySelector('li:last-child');
It does actually return the last li element within the nested ul list, so why doesn't the first option work to simply get the 'a' element within that?
Here are some other things I tried which do work:
let lastLink = item.querySelector('#item li:last-child > a');
However, since I already have a reference to the base element it seems pointless to specify its ID here. These also work and return the correct a element:
let lastLink = item.querySelector('li ul li:last-child > a');
let lastLink = item.querySelector('li:last-child li:last-child > a');
My question is, why doesn't the 1st option work? Appreciate if anyone can throw some light on the reason
CodePudding user response:
Doing rootElm.querySelector(selector)
essentially does:
for (const node of recursiveDescendantNodes) {
if (node.matches(selector)) {
return node;
}
}
The selector
is not broken down and analyzed to see if the first part of the selector starts lower than the root node - usually, all that specifying the root node does is indicate which descendants to search through.
Since the first <a>
that points to #foo
matches the selector passed, it's returned.
console.log(
document.querySelector('#item > a').matches('li:last-child > a')
);
<ul>
<li id="item">
<a href="#foo">Stuff</a>
<ul>
<li> <a href="#foo">Foo stuff</a></li>
<li><a href="#bar">Bar Stuff</a></li>
<li><a href="#foobar">Foo-Bar Stuff</a></li>
</ul>
</li>
</ul>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
There is a way to get around that behavior, though, by using :scope
- which, in supported browsers, indicates "the element querySelector
(or querySelectorAll
) was called on", which is the functionality you're hoping to see.
let item = document.querySelector('#item');
let lastLink = item.querySelector(':scope li:last-child > a');
console.log(lastLink);
<ul>
<li id="item">
<a href="#foo">Stuff</a>
<ul>
<li> <a href="#foo">Foo stuff</a></li>
<li><a href="#bar">Bar Stuff</a></li>
<li><a href="#foobar">Foo-Bar Stuff</a></li>
</ul>
</li>
</ul>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>