2

What's the modern, concise, and fast way to test whether a node has any child that matches a given selector?

By "concise" I mean something similar to jQuery or functional-style, such as avoiding loops. I know native selectors are getting more and more of this type of thing but have not kept up with developments. If such does not yet exist across browsers then I also want to know.

I expected it to be straightforward but searching Google and SO find many false hits using jQuery or finding arbitrary descendants at any depth rather than just the immediate children. There are also some outdated questions from before many functional-style methods were added and standardized between browsers.

Maximillian Laumeister
  • 19,884
  • 8
  • 59
  • 78
hippietrail
  • 15,848
  • 18
  • 99
  • 158

4 Answers4

6

One option is to use the direct child combinator, >, and the :scope pseudo-class:

var children = parentElement.querySelectorAll(':scope > div');

var parentElement = document.querySelector('.container');
var children = parentElement.querySelectorAll(':scope > div');

for (var i = 0; i < children.length; i++) {
  children[i].style.background = '#f00';
}
.level2 { background-color: #fff; }
<div class="container">
  <span>Span</span>
  <span>Span</span>
  <div class="level1">Direct 'div'
    <div class="level2">Nested 'div'</div>
  </div>
  <div class="level1">Direct 'div'
    <div class="level2">Nested 'div'</div>
  </div>
  <div class="level1">Direct 'div'
    <div class="level2">Nested 'div'</div>
  </div>
</div>

Note that the :scope pseudo-class is still considered experimental and does not have full browser support. But nonetheless, it is probably the most "modern" solution (as you asked for).


Alternatively, you could use the .filter() method and check whether the parent element's children match a given selector:

function getChildren(parent, selector) {
  return Array.prototype.filter.call(parent.children, function(node) {
    return node.matches(selector);
  });
}

Usage:

getChildren(parentElement, 'div'); // Direct children 'div' elements

function getChildren(parent, selector) {
  return Array.prototype.filter.call(parent.children, function(node) {
    return node.matches(selector);
  });
}

var parentElement = document.querySelector('.container');
var children = getChildren(parentElement, 'div');

for (var i = 0; i < children.length; i++) {
  children[i].style.background = '#f00';
}
.level2 { background-color: #fff; }
<div class="container">
  <span>Span</span>
  <span>Span</span>
  <div class="level1">Direct 'div'
    <div class="level2">Nested 'div'</div>
  </div>
  <div class="level1">Direct 'div'
    <div class="level2">Nested 'div'</div>
  </div>
  <div class="level1">Direct 'div'
    <div class="level2">Nested 'div'</div>
  </div>
</div>
Josh Crozier
  • 233,099
  • 56
  • 391
  • 304
  • 2
    It looks like `:scope` isn't supported in Chrome yet unless either of two experimental flags is set, according to the link you gave me. – hippietrail Jan 27 '16 at 02:45
  • In fact I don't even need to get the children, I only need to check whether any matching ones exist. I'm not sure whether this changes much but might make the code a little more concise. Let me know if this wasn't clear in the question as I'm still tweaking it as I read everybody's answers and comments. – hippietrail Jan 27 '16 at 03:23
  • @hippietrail But wouldn't you still need to somehow get the children elements in order to check whether they exist? – Josh Crozier Jan 27 '16 at 03:29
  • It appears that is currently the case but before seeing all the responses I might've expected some kind of "hasa" method. Other than that I assume returning true or false would be faster than returning an array. I might be wrong. – hippietrail Jan 27 '16 at 03:39
  • 1
    @hippietrail It's worth mentioning that there is a [`.contains()` method](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains), but that checks for all descendants (rather than just children elements). There is also a [`.hasChildNodes()` method](https://developer.mozilla.org/en-US/docs/Web/API/Node/hasChildNodes), but it doesn't accept any arguments (it just returns a boolean based on whether there are *any* children nodes). I am not aware of any other native methods for checking. – Josh Crozier Jan 27 '16 at 03:43
3

A solution with a wider browser support:

[].some.call(yourEl.children, function(e){return e.matches(".z")})

In terms of conciseness, in ES2015 (obviously, by using a transpiler) it would be even better, with arrow functions:

[].some.call(yourEl.children, e=>e.matches(".z"))

And with Array.from (ES2015):

Array.from(yourEl.children).some(e=>e.matches(".z"))

Or, in an utility function:

function childMatches(elmt, selector){
  return [].some.call(elmt.children, function(e){
    return e.matches(selector);
  });
}

Usage

childMatches(yourElement, ".any-selector-you-want")

Alcides Queiroz
  • 9,456
  • 3
  • 28
  • 43
  • OK so there's not a concise way in vanilla that works across browsers just yet. That's good to know. – hippietrail Jan 27 '16 at 02:50
  • 2
    @hippietrail Yeap. The best approach certainly would be using `:scope`, as exposed by Josh in his answer, but it's experimental yet. – Alcides Queiroz Jan 27 '16 at 02:57
  • I'm unclear on why there is `el.matches` as well as `el.matches(selector)`. Is this because `.matches()` might not exist on some platforms? If that's the case wouldn't these silently fail? If that's not the case then what am I missing? ... Looks like you were removing that exactly as I was typing this (-: – hippietrail Jan 27 '16 at 03:49
  • 1
    I removed this guard syntax in my last edit, since by replacing `childNodes` with `children` I don't need to deal with text nodes anymore (the reason why I was checking for the `matches` method). – Alcides Queiroz Jan 27 '16 at 03:53
  • 1
    OK this is now my favourite method though Josh's answer is also good. I won't accept either just yet as I'm still watching the votes and other changes come in. Thanks for this! – hippietrail Jan 27 '16 at 03:57
1

Use the child selector >

document.querySelectorAll('.parent-selector > .child-selector').length > 0
Evan Davis
  • 35,493
  • 6
  • 50
  • 57
  • 2
    I'm starting with the parent and not with the document. Sorry if this is not clear in the question as I had hoped it would be. – hippietrail Jan 27 '16 at 03:35
0

If you want to apply a selector starting from a specific node but can't assume :scope support, you could build a selector to a specific node like this

function selectorPath(node) {
    var idx;
    if (node.nodeName === 'HTML' || !node.parentNode) return node.nodeName;
    idx = Array.prototype.indexOf.call(node.parentNode.children, node);
    return selectorPath(node.parentNode) + ' > ' + node.nodeName + ':nth-child(' + (idx + 1) + ')';
}

Then using this in a multi-part selector might look like this

function selectChildAll(parent, selector) {
    var pSelector = selectorPath(parent) + ' > ';
    selector = pSelector + selector.split(/,\s*/).join(', ' + pSelector);
    return parent.querySelectorAll(selector);
}

So an example of using it might be, to get all <p> and <pre> immediate children from this answer's content,

var node = document.querySelector('#answer-35028023 .post-text');
selectChildAll(node, 'p, pre'); // [<p>​…​</p>​, etc​]
Paul S.
  • 64,864
  • 9
  • 122
  • 138