hippietrail
hippietrail

Reputation: 16974

Modern, concise, vanilla JS check whether node has a direct child matching a selector

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.

Upvotes: 2

Views: 1489

Answers (4)

Josh Crozier
Josh Crozier

Reputation: 241048

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>

Upvotes: 6

Alcides Queiroz
Alcides Queiroz

Reputation: 9576

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")

Upvotes: 3

Paul S.
Paul S.

Reputation: 66364

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​]

Upvotes: 0

Evan Davis
Evan Davis

Reputation: 36592

Use the child selector >

document.querySelectorAll('.parent-selector > .child-selector').length > 0

Upvotes: 1

Related Questions