Weird behaviour when using querySelector - javascript

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?

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.

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.

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.

Related

How to make a selector for direct child of the root node?

Consider that you are given a node node and you must provide all direct children given by selector Direct. Selector for direct child is:
childParent > directChild
However, the following fails with an error in console:
document.body.querySelectorAll(">div")
SyntaxError: '>div' is not a valid selector
I have a function that needs to do something on select direct child nodes, but I'm not sure how to handle this. Except of course using the for loop and analyse the children with my own code, abandoning selectors completely.
The following code does not work. Can it be changed so that it does what is intended?
function doWithDirectChildren(parentNode) {
// does not work, the selector is invalid
const children = parentNode.querySelector(">.shouldBeAffected");
for(const direct of children) {
// do something with the direct child
}
}
I'm asking for a solution, not a workaround.
The proper way to do this is with the :scope psuedo class.
According to the documentation at MDN:
When used from a DOM API such as querySelector(), querySelectorAll(), matches(), or Element.closest(), :scope matches the element on which the method was called.
For example:
let parent = document.querySelector('#parent');
let scoped = parent.querySelectorAll(':scope > span');
Array.from(scoped).forEach(s => {
s.classList.add('selected');
});
.selected {
background: yellow;
}
<div id="parent">
<span> Select Me </span> <br>
<span> Me Too </span>
</div>
<span> Not Selected </span>
The child combinator operator > is a binary operator so using it with nothing on the left side is invalid.
The child combinator (>) is placed between two CSS selectors. It
matches only those elements matched by the second selector that are
the direct children of elements matched by the first.
If you can provide individual parent and child selectors you can do something simple like this
let directChildren = (parent, child) => document.querySelectorAll(`${parent} > ${child}`);
directChildren('body','div');//...
If your parent argument is a node or collection you would have to use a method of converting it back to a selector, like this one
jQuery solves this problem in 2 ways. Consider this code:
$('div.root').find('> .p2').addClass('highlighted');
$('div.root').children('.p2').addClass('red');
.highlighted {
background: yellow
}
.red {
color: red
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="root">
<p>div 1</p>
<p class="p2">paragraph 2</p>
<p>paragraph 3</p>
<div>
<p class="p2">paragraph 2 2</p>
</div>
</div>
using .find('> selector) finds only direct children matching the selector, and using .children('selector') also does the same.

Get Parent of DOM Element Using JavaScript

I’d like to check whether a DOM element is a child of a specific DIV class regardless of the number of DIV/HTML between the element and the parent DIV. I need to use JavaScript (not jQuery). So if I had some HTML like this:
<div class="header grid-12">
<!-- many levels of divs/html -->
<div class="section">
<span id="id1">hello</span>
</div>
</div>
<div class="footer">
<!-- many levels of divs/html -->
<span id="id2">goodbye</span>
</div>
I'd want to do something like this (logically that is):
var domID = document.getElementById("id1");
if (domID a child of 'header grid-12') {
console.log('header grid-12 found');
}
I looked at parentNode children which would allow you to get all of the child nodes but I need to loop in reverse (parentnode parent if you will). I'm thinking it's much faster to start at the child and go up as opposed starting at "header grid-12" and looping through hundreds/thousands of nodes.
Thanks
The Element.closest() method returns the closest ancestor of the current element (or the current element itself) which matches the selectors given in parameter. If there isn't such an ancestor, it returns null.
source
try this,
let parent = !!document.getElementById('id1').closest('.header.grid-12');
if(parent)
{
console.log('parent found');
}
domElement.closest('selector') goes in reverse and return the nearest matching parent element. This will save you from iteratiing through all domChildElements.

querySelector :scope is null

I am trying to select a sibling of the current element, but when I use a.querySelector(':scope') on the element it is null. When I try to add a sibling selector to the :scope it is still null, but if I use it within .matches(':scope') it returns true. How can I use the current element within the selector?
let a = document.querySelector('div > a')
console.log(a.querySelector(':scope'))
console.log(a.querySelector(':scope + span'))
console.log(a.matches(':scope'))
<div>
<span></span>
</div>
querySelector selects the descendants of the context element.
The element itself won't be a descendant of itself, and nor will any of its siblings.
You can use :scope if you are targetting a descendant.
For example, to search only the children and not deeper descendants you can use :scope with the child combinator.
let a = document.querySelector('#foo')
console.log(a.querySelector(':scope > span'))
<div id="foo">
<div><span>2</span></div>
<span>1</span>
</div>

How to recursively select all children under an element Javascript

I need a function that recursively selects all child elements but wont select elements (and those elements children) if they have the "foo" attribute.
<section>
<div>
<span foo>
<input>
</span>
</div>
<p>
<img>
</p>
</section>
//should return [section, div, p, img]
I need raw Javascript please
edit:
I tried something like this:
$tag.querySelectorAll(":not([foo])")
but querySelectorAll(":not([foo])") will still return the children of the unselected element.
You can use element.querySelectorAll() with the :not modifier here, together with the [attr] selector:
var nonFooElements = parentElement.querySelectorAll("*:not([foo])");
Be aware that this sort of call will be moderately expensive because the selector doesn't begin with an id or a classname, so don't do huge amounts of it.
I adapted this answer from this one: https://stackoverflow.com/a/21975970/5009210

About querySelector() with multiple selectors

I had a situation in which I wanted to focus either an input tag, if it existed, or it's container if it didn't. So I thought of an intelligent way of doing it:
document.querySelector('.container input, .container').focus();
Funny, though, querySelector always returns the .container element.
I started to investigate and came out that, no matter the order in which the different selectors are put, querySelector always returns the same element.
For example:
var elem1 = document.querySelector('p, div, pre');
var elem2 = document.querySelector('pre, div, p');
elem1 === elem2; // true
elem1.tagName; // "P".
My question is: What are the "reasons" of this behavior and what "rules" (if any) make P elements have priority over DIV and PRE elements.
Note: In the situation mentioned above, I came out with a less-elegant but functional solution:
(document.querySelector('.container input') ||
document.querySelector('.container') ).focus();
document.querySelector returns only the first element matched, starting from the first element in the markup. As written on MDN:
Returns the first element within the document (using depth-first
pre-order traversal of the document's nodes|by first element in
document markup and iterating through sequential nodes by order of
amount of child nodes) that matches the specified group of selectors.
If you want all elements to match the query, use document.querySelectorAll (docs), i.e. document.querySelectorAll('pre, div, p'). This returns an array of the matched elements.
The official document says that,
Returns the first element within the document (using depth-first pre-order traversal of the document's nodes|by first element in document markup and iterating through sequential nodes by order of amount of child nodes) that matches the specified group of selectors.
So that means, in your first case .container is the parent element so that it would be matched first and returned. And in your second case, the paragraph should be the first element in the document while comparing with the other pre and div. So it was returned.
That's precisely the intended behavior of .querySelector() — it finds all the elements in the document that match your query, and then returns the first one.
That's not "the first one you listed", it's "the first one in the document".
This works, essentially, like a CSS selector. The selectors p, div, pre and pre, div, p are identical; they both match three different types of element. So the reason elem1.tagName == 'P' is simply that you have a <p> on the page before any <pre> or <div> tags.
You can try selecting all elements with document.querySelectorAll("p.a, p.b") as shown in the example below and using a loop to focus on all elements that are found.
<html>
<body>
<p class="a">element 1</p>
<p class="b">element 2</p>
<script>
var list=document.querySelectorAll("p.a, p.b");
for (let i = 0; i < list.length; i++) {
list[i].style.backgroundColor = "red";
}
</script>
</body>
</html>

Categories