Using querySelectorAll to retrieve direct children
Asked Answered
S

15

284

I am able to do this:

<div id="myDiv">
   <div class="foo"></div>
</div>
myDiv = getElementById("myDiv");
myDiv.querySelectorAll("#myDiv > .foo");

That is, I can successfully retrieve all the direct children of the myDiv element that have class .foo.

The problem is, it bothers me that I must include the #myDiv in the selector, because I am running the query on the myDiv element (so it is obviously redundant).

I ought to be able to leave the #myDiv off, but then the selector is not legal syntax since it starts with a >.

Does anyone know how to write a selector which gets just the direct children of the element that the selector is running on?

Spa answered 9/9, 2010 at 21:57 Comment(2)
checkout #6482112Cocke
Did none of the answers accomplish what you needed? Please provide feedback or select an answer.Iden
J
344

Good question. At the time it was asked, a universally-implemented way to do "combinator rooted queries" (as John Resig called them) did not exist.

Now the :scope pseudo-class has been introduced. It is not supported on [pre-Chrominum] versions of Edge or IE, but has been supported by Safari for a few years already. Using that, your code could become:

let myDiv = getElementById("myDiv");
myDiv.querySelectorAll(":scope > .foo");

Note that in some cases you can also skip .querySelectorAll and use other good old-fashioned DOM API features. For example, instead of myDiv.querySelectorAll(":scope > *") you could just write myDiv.children, for example.

Otherwise if you can't yet rely on :scope, I can't think of another way to handle your situation without adding more custom filter logic (e.g. find myDiv.getElementsByClassName("foo") whose .parentNode === myDiv), and obviously not ideal if you're trying to support one code path that really just wants to take an arbitrary selector string as input and a list of matches as output! But if like me you ended up asking this question simply because you got stuck thinking "all you had was a hammer" don't forget there are a variety of other tools the DOM offers too.

Juvenescence answered 20/6, 2013 at 5:45 Comment(6)
myDiv.getElementsByClassName("foo") isn't the same as myDiv.querySelectorAll("> .foo"), it's more like myDiv.querySelectorAll(".foo") (which actually works by the way) in that it finds all descendant .foos as opposed to just children.Raynor
Oh, bother! By which I mean: you're exactly right. I've updated my answer to make it more clear that it's not a very good one in the OP's case.Juvenescence
thanks for the link (that seems to answer it definitively), and thanks for adding the terminology "combinator rooted queries".Spa
What John Resig calls "combinator-rooted queries", Selectors 4 now calls them relative selectors.Raynor
Also, :root is available.Patrolman
:scope now appears to be implemented in all major browsers except Internet Explorer (which was replaced by MS Edge), so it can be used without reservation unless you need to support IE.Udder
P
161

Does anyone know how to write a selector which gets just the direct children of the element that the selector is running on?

The correct way to write a selector that is "rooted" to the current element is to use :scope.

var myDiv = getElementById("myDiv");
var fooEls = myDiv.querySelectorAll(":scope > .foo");

However, browser support is limited and you'll need a shim if you want to use it. I built scopedQuerySelectorShim for this purpose.

Partition answered 15/1, 2014 at 0:33 Comment(5)
Its worth mentioning that the :scope spec is currently a "Working Draft" and therefore subject to change. Its likely it will still work like this if/when it is adopted, but a bit early to say this is the "correct way" to do it in my opinion.Zymosis
Looks like it has been deprecated in the meantimeKilometer
3 years later and you save my gluteus maximus!Parodist
@Adrian Moisa: :scope hasn't been deprecated anywhere. You may be getting it mixed up with the scoped attribute.Raynor
FWIW this seems to be fully supported everywhere except IE nowadays.Saracen
M
11

I Use This:

You can avoid typing "myDiv" twice AND using the arrow.
There are of course always more possibilities.
A modern browser is probably required.


<!-- Sample Code -->

<div id="myDiv">
    <div class="foo">foo 1</div>
    <div class="foo">foo 2
        <div class="bar">bar</div>
    </div>
    <div class="foo">foo 3</div>
</div>

// Return HTMLCollection (Matches 3 Elements)

var allMyChildren = document.querySelector("#myDiv").children;

// Return NodeList (Matches 7 Nodes)

var allMyChildren = document.querySelector("#myDiv").childNodes;

// Match All Children With Class Of Foo (Matches 3 Elements)

var myFooChildren = document.querySelector("#myDiv").querySelectorAll(".foo");

// Match Second Child With Class Of Foo (Matches 1 Element)

var mySecondChild = document.querySelector("#myDiv").querySelectorAll(".foo")[1];

// Match All Children With Class Of Bar (Matches 1 Element)

var myBarChild = document.querySelector("#myDiv").querySelector(".bar");

// Match All Elements In "myDiv" (Matches 4 Elements)

var myDescendants = document.querySelector("#myDiv").querySelectorAll("*");

Manilla answered 12/2, 2021 at 19:18 Comment(0)
I
9

if you know for sure the element is unique (such as your case with the ID):

myDiv.parentElement.querySelectorAll("#myDiv > .foo");

For a more "global" solution: (use a matchesSelector shim)

function getDirectChildren(elm, sel){
    var ret = [], i = 0, l = elm.childNodes.length;
    for (var i; i < l; ++i){
        if (elm.childNodes[i].matchesSelector(sel)){
            ret.push(elm.childNodes[i]);
        }
    }
    return ret;
}

where elm is your parent element, and sel is your selector. Could totally be used as a prototype as well.

Iden answered 7/12, 2012 at 17:54 Comment(6)
@Partition that is not part of the question.Iden
Your answer is not a "global" solution. It has specific limitations that should be noted, hence my comment.Partition
@Partition which answers the question asked. So why the down-vote? Or did you just happen to comment within seconds of the down-vote on the year-old answer?Iden
The solution uses matchesSelector unprefixed (which doesn't even work in the latest version of Chrome), it pollutes the global namespace (ret was not declared), it doesn't return a NodeList like querySelectorMethods are expected to. I don't think it's a good solution, hence the downvote.Partition
@Partition - The original question uses unprefixed function and global namespace. It's a perfectly fine workaround for the question given. If you don't think it's a good solution, don't use it, or suggest an edit. If it's functionally incorrect or spam, then consider a downvote.Iden
Thanks for improving your solution, it no longer leaks globals. I've removed my downvote and added my own solution, which directly addresses the question ("how to write a selector"). It's linted, tested and works truly globally as a shim implementing an accepted web standard, :scope.Partition
C
7

Here's a flexible method, written in vanilla JS, that allows you to run a CSS selector query over only the direct children of an element:

var count = 0;
function queryChildren(element, selector) {
  var id = element.id,
      guid = element.id = id || 'query_children_' + count++,
      attr = '#' + guid + ' > ',
      selector = attr + (selector + '').replace(',', ',' + attr, 'g');
  var result = element.parentNode.querySelectorAll(selector);
  if (!id) element.removeAttribute('id');
  return result;
}
Conti answered 29/7, 2013 at 18:32 Comment(8)
I would suggest you add some kind of timestamp or counter to the ids you generate. There is a non-zero (albeit tiny) probability for Math.random().toString(36).substr(2, 10) to produce the same token more than once.Vanir
I'm not sure how likely that is given the odds of repeat hashes is minuscule, the ID is only temporarily applied for a fraction of a millisecond, and any dupe would need to also be a child of the same parent node - basically, the chances are astronomical. Regardless, adding a counter seems fine.Conti
Not sure what you mean by any dupe would need to also be a child of the same parent node, id attributes are document-wide. You're right the odds are still quite negligible, but thank you for taking the high road and adding that counter :)Vanir
JavaScript is not executed in parallel, so the chances are in fact zero.Monomolecular
@Monomolecular Wow, I can't believe it, I'm absolutely stunned I would brain fart on such a simple point (I work at Mozilla and recite this fact often, gotta get more sleep! LOL)Conti
This won't work if element is not currently in the DOM (as in the case of element = document.createElement('div').Partition
I figured that assumption was clear, but you could detect the lack of a parentNode and temporarily wrap the target node with an element to query from.Conti
@csuwldcat, I've done exactly that in a shim for querySelector that implements :scope. See my answer https://mcmap.net/q/107671/-using-queryselectorall-to-retrieve-direct-children.Partition
T
4
function siblings(el) {
    return el.closest('*:not(:scope)').querySelectorAll(':scope > *');
}

Pure JS Code

el - is your element here we have function which works in next steps:

  1. take any direct parent, but not el which we are passed (closest can take el, so we are ensure el will not be as a result)
  2. take all direct children
Townsfolk answered 8/7, 2021 at 6:49 Comment(1)
Instead of .closest('*:not(:scope)'), why not simply use .parentNode?Boesch
C
3

The following solution is different to the ones proposed so far, and works for me.

The rationale is that you select all matching children first, and then filter out the ones which are not direct children. A child is a direct child if it does not have a matching parent with the same selector.

function queryDirectChildren(parent, selector) {
    const nodes = parent.querySelectorAll(selector);
    const filteredNodes = [].slice.call(nodes).filter(n => 
        n.parentNode.closest(selector) === parent.closest(selector)
    );
    return filteredNodes;
}

HTH!

Cohn answered 29/10, 2017 at 18:44 Comment(1)
I like this for its brevity, and the simplicity of the approach, although it would most likely not perform well if called at high frequency with large trees and overly greedy selectors.Makeshift
B
1

I created a function to handle this situation, thought I would share it.

getDirectDecendent(elem, selector, all){
    const tempID = randomString(10) //use your randomString function here.
    elem.dataset.tempid = tempID;

    let returnObj;
    if(all)
        returnObj = elem.parentElement.querySelectorAll(`[data-tempid="${tempID}"] > ${selector}`);
    else
        returnObj = elem.parentElement.querySelector(`[data-tempid="${tempID}"] > ${selector}`);

    elem.dataset.tempid = '';
    return returnObj;
}

In essence what you are doing is generating a random-string (randomString function here is an imported npm module, but you can make your own.) then using that random string to guarantee that you get the element you are expecting in the selector. Then you are free to use the > after that.

The reason I am not using the id attribute is that the id attribute may already be used and I don't want to override that.

Beauty answered 15/6, 2016 at 21:38 Comment(0)
E
1

You could extend Element to include a method getDirectDesc() like this:

Element.prototype.getDirectDesc = function() {
    const descendants = Array.from(this.querySelectorAll('*'));
    const directDescendants = descendants.filter(ele => ele.parentElement === this)
    return directDescendants
}

const parent = document.querySelector('.parent')
const directDescendants = parent.getDirectDesc();

document.querySelector('h1').innerHTML = `Found ${directDescendants.length} direct descendants`
<ol class="parent">
    <li class="b">child 01</li>
    <li class="b">child 02</li>
    <li class="b">child 03 <ol>
            <li class="c">Not directDescendants 01</li>
            <li class="c">Not directDescendants 02</li>
        </ol>
    </li>
    <li class="b">child 04</li>
    <li class="b">child 05</li>
    </ol>
    <h1></h1>
Expository answered 21/2, 2021 at 9:0 Comment(0)
S
0

Well we can easily get all the direct children of an element using childNodes and we can select ancestors with a specific class with querySelectorAll, so it's not hard to imagine we could create a new function that gets both and compares the two.

HTMLElement.prototype.queryDirectChildren = function(selector){
  var direct = [].slice.call(this.directNodes || []); // Cast to Array
  var queried = [].slice.call(this.querySelectorAll(selector) || []); // Cast to Array
  var both = [];
  // I choose to loop through the direct children because it is guaranteed to be smaller
  for(var i=0; i<direct.length; i++){
    if(queried.indexOf(direct[i])){
      both.push(direct[i]);
    }
  }
  return both;
}

Note: This will return an Array of Nodes, not a NodeList.

Usage

 document.getElementById("myDiv").queryDirectChildren(".foo");
Stagecoach answered 27/6, 2017 at 20:45 Comment(0)
M
0

I would like to add that you can extend the compatibility of :scope by just assigning a temporary attribute to the current node.

let node = [...];
let result;

node.setAttribute("foo", "");
result = window.document.querySelectorAll("[foo] > .bar");
// And, of course, you can also use other combinators.
result = window.document.querySelectorAll("[foo] + .bar");
result = window.document.querySelectorAll("[foo] ~ .bar");
node.removeAttribute("foo");
Monophony answered 29/8, 2018 at 22:56 Comment(0)
G
0

Single line Version

var direct_children=Array.from(parent_el.querySelectorAll('span')).filter(function(a){return a.parentNode===parent_el;});

I found this is very handy in case parent element is given. I tested it and it worked 100%.

Greywacke answered 11/6, 2021 at 4:5 Comment(0)
B
0

I was working on Bootstrap and I had the same question and I used the below js to help me

let items = document.querySelectorAll(
  ".multi-item-carousel  .carousel-inner .carousel-item"
);

My HTML looks like

      <div
        id="recipeCarousel"
        className="carousel slide multi-item-carousel"
        data-bs-ride="carousel"
      >
        <div className="carousel-inner" role="listbox">
          <div className="carousel-item active">
Baltic answered 10/11, 2023 at 8:50 Comment(0)
A
-1

I'd have gone with

var myFoo = document.querySelectorAll("#myDiv > .foo");
var myDiv = myFoo.parentNode;
Asperse answered 25/7, 2016 at 12:54 Comment(0)
D
-3

I am just doing this without even trying it. Would this work?

myDiv = getElementById("myDiv");
myDiv.querySelectorAll(this.id + " > .foo");

Give it a try, maybe it works maybe not. Apolovies, but I am not on a computer now to try it (responding from my iPhone).

Darin answered 9/11, 2012 at 22:55 Comment(1)
QSA is scoped to the element it's being called on, so this would look for another element inside myDiv with the same ID as myDiv, then its children with .foo. You could do something like `document.querySelectorAll('#' + myDiv.id + ' > .foo');Palaestra

© 2022 - 2024 — McMap. All rights reserved.