21

Here's the markup i'm trying to query. So given the markup:

<table class="non-unique-identifier table">
<tr><td><div id="unique-identifier"></div></td></tr>
</table>

I'm querying for #unique-identifier:

var myDiv = document.getElementById('#unique-identifier');

I'm then trying to select the table. The issue is that i want to make the code not brittle so i don't need to do this:

var myDiv = document.getElementById('#unique-identifier'),
    myTable = myDiv.parentNode.parentNode.parentNode.parentNode;

My question

Is there currently a DOM implementation of the jQuery equivalent of $().closest() ? A closest implementation that is efficient without nested for loops would be preferred.

Limitations

I'm required to not use jQuery or sizzle for this particular issue or introduce any new libraries. The code is quite old as well. Thus, that is the reason for such limitations and the existence of <tables>.

chrisjlee
  • 21,691
  • 27
  • 82
  • 112

8 Answers8

35

You can't do this without a loop :

function closest (el, predicate) {
  do if (predicate(el)) return el;
  while (el = el && el.parentNode);
}

Well, actually you can, using recursivity (a disguised loop) :

function closest(el, predicate) {
  return predicate(el) ? el : (
     el && closest(el.parentNode, predicate)
  );
}

A demo (using Sizzle for the DOM queries) :

// s = selectors
// n = number of selectors
// get closest s[i+1] from s[i]
// where 0 <= i < n and i % 2 = 0

function main (s) {
  var i, el, from;
  var n = s.length;
  for (i = 0; i < n; i += 2) {
    from = Sizzle(s[i])[0];
    el = closest(from, function (el) {
      return !!el && el !== document && (
        Sizzle.matchesSelector(el, s[i + 1])
      );
    });
    console.log(el);
  }
}

function closest (el, predicate) {
  do if (predicate(el)) return el;
  while (el = el && el.parentNode);
}

main([
  "#winner" , "b", 
  "#winner" , "p", 
  "#winner" , "div", 
  "#winner" , "div:not(#trump)", 
  "#winner" , "#clinton",
  "#looser" , "html"
]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/sizzle/1.10.18/sizzle.min.js"></script>

<div id="main">
  <div id="trump">
    <p>Donald <b id="winner">Trump</b></p>
  </div>
  <div id="clinton">
    <p>Hillary <b>Clinton</b></p>
  </div>
</div>
  • `return el.classList.contains('item')` would be better in case of having multiple classNames. – korsun Feb 16 '16 at 03:16
18

To add an updated answer, there is now Element.closest(<query_selector>) available.

https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

This isn't supported on IE, but that mozilla doc page includes code for a polyfill for IE8 and IE9+.

Michael S
  • 726
  • 1
  • 10
  • 23
  • 4
    Although it wasn't available 5 years ago when the OP asked, now that a native solution is available on nearly every browser (and a standard polyfill for the rest), this should absolutely be the preferred approach for any real implementation, though the other answers are interesting for academic / educational purposes. – jacobq Feb 25 '19 at 22:11
  • now this solution is the best way to do... jquery deprecation is imminent – ensarman Mar 12 '20 at 06:12
5

Concise and quick (tested with Benchmark.js) way to search for closest element by any css selector:

var ep = Element.prototype;
ep.matches = ep.matches || ep.webkitMatchesSelector || ep.msMatchesSelector || ep.mozMatchesSelector;

function getClosest( elem, selector ) {
    while (elem !== document.body) {
        elem = elem.parentElement;
        if (elem.matches(selector)) return elem;
    }
}

Supports IE9+ and the rest of the browsers you can expect to care about.

antitoxic
  • 3,746
  • 1
  • 37
  • 46
3

function closestById(el, id) {
  while (el.id != id) {
    el = el.parentNode;
    if (!el) {
      return null;
    }
  }
  return el;
}

// Use it like:

yourTarget = closestById(document.getElementById('unique-identifier'),'targetId')
alert(yourTarget.id);
<div id="targetId">
  Finish
  <div>
    <div id="unique-identifier">
      Start
    </div>
  </div>
</div>

This searches upwards, until a certain ID is found. You can also alter the code to find certain classes.

fboes
  • 2,149
  • 16
  • 17
2

Alternative is a recursive function. This is slightly different to closest as i searches the children, I'm not sure if closest does.

function closest(elem) {
    if( elem.className.indexOf("non-unique-identifier") ) {
        return elem;
    } 

    var parent = elem.parentNode;

    for(var i = 0; i< parent.children.length; i++ ) {
        if( parent.children[i].className.indexOf("non-unique-identifier")!=-1)  {
            return parent.children[i];
        }
    }

    return closest(parent);
}



var elem = document.getElementById('unique-identifier');

var cl = closest(elem);

console.log(cl);

Non children searching example (more like closest):

function closest(elem) {
    if( elem.className.indexOf("non-unique-identifier") ) {
        return elem;
    } 

    var parent = elem.parentNode;

    if( parent.className.indexOf("non-unique-identifier")!=-1) {
        return parent;
    }    

    return closest(parent);
}



var elem = document.getElementById('unique-identifier');

var cl = closest(elem);

console.log(cl);
MasNotsram
  • 2,105
  • 18
  • 28
2
<div class="item">
  <div class="itemed">
    <p>Hello <b class="itemed" id="world">World</b></p>
  </div>
</div>

function closest(el, classname) {
   if(el.parentNode){
        if(el.parentNode.className.includes(classname)){
        return el.parentNode;
      }
      else{
        return closest(el.parentNode, classname);
      }
   }
   else{
    return false;
   }
}

var world = document.getElementById('world');
var closest = closest(world, 'item');
console.log(closest);
Jeffrey Roosendaal
  • 6,872
  • 8
  • 37
  • 55
Uday Hiwarale
  • 4,028
  • 6
  • 45
  • 48
2

In this case, use JavaScript's closest()

Exemple:

var el = document.getElementById('div-03');

var r1 = el.closest("#div-02");  
// returns the element with the id=div-02

var r2 = el.closest("div div");  
// returns the closest ancestor which is a div in div, here is div-03 itself

var r3 = el.closest("article > div");  
// returns the closest ancestor which is a div and has a parent article, here is div-01

var r4 = el.closest(":not(div)");
// returns the closest ancestor which is not a div, here is the outmost article
<article>
  <div id="div-01">Here is div-01
    <div id="div-02">Here is div-02
      <div id="div-03">Here is div-03</div>
    </div>
  </div>
</article>

You can find more information, on MDN Web Docs by clicking here

  • 2
    This solution, although a great one, has some pretty significant browser compatibility issues i.e. no IE, no IE Mobile, no Edge < 14... will require a polyfill for those browsers. https://caniuse.com/#feat=element-closest – Martin James May 12 '19 at 10:12
1

I wrote this simple function in one of my TypeScript projects so i used querySelector on parentNode so you can pass to function class, id, tag name etc

findParentNode(el, selector:string):Element | null {
  let found = null;
  let child = el;
  let childSelector = guessSelector(child);

  while(child !== document && found === null) {
    child = child.parentNode;
    childSelector = guessSelector(child);
    found = childSelector ? child.parentNode.querySelector(`${childSelector} > ${selector}`) : null;
  }

  return found;

  function guessSelector(child:any):string {
    childSelector = child.className ? `.${child.className.replace(' ', '.')}` : null;

    if (typeof child.getAttribute === 'function') {
      childSelector = !childSelector ?
        (child.getAttribute('id') ? `#${child.getAttribute('id')}` : null) : childSelector;

      childSelector = !childSelector ?
        child.tagName.toLowerCase() : childSelector;
    }

    return childSelector;
  }
}

Example:

If you want to find closest parent of target which has .param-input class you can do this like this:

document.body.addEventListener('click', (e) => {
  console.log(findParentNode(e.target, '.param-input'));
});
falsarella
  • 12,217
  • 9
  • 69
  • 115