Closest ancestor matching selector using native DOM?
Asked Answered
A

6

17

Is anybody working on a jQuery.closest() equivalent in the DOM api?

Looks like the Selectors Level 2 draft adds matches() equivalent to jQuery.is(), so native closest should be much easier to write. Has adding closest() to Selectors come up?

Antipater answered 10/3, 2013 at 23:49 Comment(2)
It "should be easier" only if all browsers implement the method—they don't. :-( So a longhand method must be written anyway, I guess with feature detection for a macthes fork. It wouldn't be hard, but likely a little slow.Lyceum
Yeah, modernizer has an example of falling back to vendor prefixed matchesSelector.Antipater
G
8

See the element.closest() documentation.

Implementing such function with Element.matches() seems not optimal in terms of performance, cause apparently matches() will make a call to querySelectorAll() every time you test a parent, while only one call is sufficient for the job.

Here's a polyfill for closest() on MDN. Note a single call to querySelectorAll()

if (window.Element && !Element.prototype.closest) {
  Element.prototype.closest = 
  function(s) {
      var matches = (this.document || this.ownerDocument).querySelectorAll(s),
          i,
          el = this;
      do {
          i = matches.length;
          while (--i >= 0 && matches.item(i) !== el) {};
      } while ((i < 0) && (el = el.parentElement)); 
      return el;
  };
}

But bear in mind that function implemented like this will not work properly on unattached tree (detached from document.documentElement root)

//Element.prototype.closestTest = function(s){...as seen above...};

var detachedRoot = document.createElement("footer");
var child = detachedRoot.appendChild(document.createElement("div"));
detachedRoot.parentElement; //null

child.closestTest("footer"); //null

document.documentElement.append(detachedRoot);
child.closestTest("footer"); //<footer>   

Though closest() that is implemented in Firefox 51.0.1 seems to work fine with detached tree

document.documentElement.removeChild(detachedRoot);
child.closestTest("footer"); //null
child.closest("footer"); //<footer>
Gramarye answered 25/2, 2017 at 15:4 Comment(1)
Seems to no longer be "experimental" according to MDN: developer.mozilla.org/en-US/docs/Web/API/Element/closestCounterreply
F
36

Building off of Alnitak's answer. Here's the working current implementation with matchesSelector which is now matches in the DOM spec.

// get nearest parent element matching selector
function closest(el, selector) {
    var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    while (el) {
        if (matchesSelector.call(el, selector)) {
            break;
        }
        el = el.parentElement;
    }
    return el;
}

Browser support is great: http://caniuse.com/matchesselector

Farthermost answered 7/5, 2013 at 23:38 Comment(5)
I'm torn. Thanks for the implementation, but does it mean native closest isn't coming?Antipater
Why .bind(ctx)(arg) rather than .call(ctx, arg) ? The former creates a new execution context, the latter doesn't.Excitement
call(elem, selector) has better performance than bind(elem)(selector), benchmark jsperf.com/native-vs-jquery-closest. Still simple .parentNode.parentNode method is highly faster.Pushbike
Why return false, not null?Muffin
Good call. Thanks yall!Farthermost
L
14

Seems like Chrome 40 will bring a native element.closest() method (http://blog.chromium.org/2014/12/chrome-40-beta-powerful-offline-and.html) specified here: https://dom.spec.whatwg.org/#dom-element-closest

Lemay answered 4/12, 2014 at 21:51 Comment(2)
element.closest has landed in Chrome 41, not 40 (see crbug.com/422731#c7). And it has also landed in Firefox 35Petulia
You can also use a polyfill to use this function on older browsers, for example github.com/jonathantneal/closestCoelho
G
8

See the element.closest() documentation.

Implementing such function with Element.matches() seems not optimal in terms of performance, cause apparently matches() will make a call to querySelectorAll() every time you test a parent, while only one call is sufficient for the job.

Here's a polyfill for closest() on MDN. Note a single call to querySelectorAll()

if (window.Element && !Element.prototype.closest) {
  Element.prototype.closest = 
  function(s) {
      var matches = (this.document || this.ownerDocument).querySelectorAll(s),
          i,
          el = this;
      do {
          i = matches.length;
          while (--i >= 0 && matches.item(i) !== el) {};
      } while ((i < 0) && (el = el.parentElement)); 
      return el;
  };
}

But bear in mind that function implemented like this will not work properly on unattached tree (detached from document.documentElement root)

//Element.prototype.closestTest = function(s){...as seen above...};

var detachedRoot = document.createElement("footer");
var child = detachedRoot.appendChild(document.createElement("div"));
detachedRoot.parentElement; //null

child.closestTest("footer"); //null

document.documentElement.append(detachedRoot);
child.closestTest("footer"); //<footer>   

Though closest() that is implemented in Firefox 51.0.1 seems to work fine with detached tree

document.documentElement.removeChild(detachedRoot);
child.closestTest("footer"); //null
child.closest("footer"); //<footer>
Gramarye answered 25/2, 2017 at 15:4 Comment(1)
Seems to no longer be "experimental" according to MDN: developer.mozilla.org/en-US/docs/Web/API/Element/closestCounterreply
E
3

This sounds like it ought to be pretty easy, given the matches function, although that's not widely supported yet:

function closest(elem, selector) {
    while (elem) {
        if (elem.matches(selector)) {
            return elem;
        } else {
            elem = elem.parentElement;
        }
    }
    return null;
}

The trouble is, the matches function isn't properly supported. As it's still a relatively new API it's available as webkitMatchesSelector in Chrome and Safari, and mozMatchesSelector in Firefox.

Excitement answered 11/3, 2013 at 0:4 Comment(2)
Yep, with *MatchesSelector, whipping up a closest using only native DOM gets easy. I'm more curious if there's any momentum behind adding element.closest(selector) as a native method of elements.Antipater
@Antipater you'd probably have to join W3C and suggest it. I suspect they wouldn't bother though, given how easy it is to implement (per the code above).Excitement
P
1

Using element.closest() we can find Closest ancestor matching selector. This method takes selectors list as parameter and returns the closest ancestor. As per Rob's Comment this API will be available from chrome 41 and FF 35.

As explained in whatwg specs https://dom.spec.whatwg.org/#dom-element-closest

Example: The below HTML will show alert message "true"

<html>
    <body>
        <foo>
            <bar>
                <a id="a">
                    <b id="b">
                        <c id="c"></c>
                    </b>
                </a>
            </bar>
         </foo>
    <script>
        var a = document.getElementById('a');
        var b = document.getElementById('b');
        var c = document.getElementById('c');
        alert(c.closest("a, b")==b);
    </script>
    </body>
</html>
Pelias answered 2/1, 2015 at 11:41 Comment(0)
A
1

A little recursion will do the trick.

// get nearest parent element matching selector
var closest = (function() {
    var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    return function closest(el, selector) {
        return !el ? null :
        matchesSelector.call(el, selector) ? el : closest(el.parentElement, selector);
    };
})();
Antenatal answered 19/8, 2015 at 13:11 Comment(1)
Or just return element.tagName === 'HTML' ? null : element.matches(selector) ? element : closest(element.parentNode, selector); ;)Operator

© 2022 - 2024 — McMap. All rights reserved.