Finding a Parent

Function to find the closest parent of an element with a given tag name, class, or ID using only vanilla JavaScript

8 months ago

Latest Post SPA's Brain by Tracy Parks

So, a while ago I had a need to find the ascendent parent from an element in the DOM. Unfortunately, I also had to use vanilla JS due to site restrictions and my own desire to do things without third-party libraries, like jQuery. After going down the stack rabbit hole, I finally found something that would do the trick. The only catch was that it only worked on class names.

function findAncestor(a,b) {
    while ((a = a.parentElement) && !a.classList.contains(b));
    return a;
}
Original: where a is the element and b is the class name as a string.

For what I needed immediately, that would do just fine, but after reusing it a few times I noticed it really came up short for scenarios where - for whatever lazy reason - the original developer made the parent element one of many in a long line of un-classed and un-ID'ed div/span elements. Worse yet, that they were dynamic and the number of children would not be predictable.

So I set the function aside and promised, one day, to come back to it when I had the time. More time did come upon me and I started noodling over how to accomplish this simple task.

The original script was simple and elegant. It is a function that is completely constructed as a while loop. All it does is keep looping up through the DOM (as seen with: a = a.parentElement) from the starting element, a,  until it can no longer not match the classList (as seen with: && !a.classList.contains(b)) of the element against the provided string, b. When it did finally find a match, the while loop stopped and the function returns the a that it found. Now this a is actually the parent of the previous a (as seen with: a = a.parentElement). That's the simplistic beauty of this little script. I know I might be praising this a bit too much, but it's small simple scripts like this that make the world go around.

Now you might be wondering, "why would you need to find an element by the ID, if you had the ID?" Well yes, that is a very good and reasonable question to ask. I actually thought along the same lines myself. "Why bother?" One can just document.querySelector('#ElementID') or the more "old school" way of document.getElementById('ElementID') to grab the element. And this is handy and a much more straight forward way of doing it, but honestly, I've run into sites that have multiple elements with the same ID. It can happen. So having the ability to go up the DOM tree and grab the specific element with a matching ID provided, is an added tool in the proverbial Dev Belt™. Also, I just thought, "why the hell not just add it."

My first approach to what I wanted was a bit - well - inelegant. I merely took out the !a.classList.contains(b) and placed it within the while loop without the not (!) and instead actually look for the match. I then placed a bunch of if...else statements in there to see if the function could match the string against the various things i wanted to look at.

function findAncestor(a,b) {
    while ((a = a.parentElement)) {
    	if (a.tagName.toLowerCase() === b.toLowerCase()) {
            return a;
            break;
        } else if (a.classList.contains(b)) {
            return a;
            break;
        } else if (a.id === b) {
            return a;
            break;
        }
    }
    return a;
}
Alpha attempt: where a is the element and b is the tag, class, or ID as a string.

‾\_(ツ)_/‾
Needless to say, although it worked, it was just "meh".

The only good thing that really came out of it was that I found that I needed to look at the tagNames in lowercase as well as place the provided b string in lowercase as the tagName returns in ALL CAPS and doesn't match if the string is given in lowercase, Pascal case, or camel case. Clearly this was an "alpha" attempt.

Deep down I knew there had to be a simpler solution, an elegant solution. So after a bit, I took a look at the original script and thought, "well if I just not match for the string, then that would bypass the need for it to be "a bunch of if...else statements. Besides, all those returns are just ghastly.

Et voilà!

function findAncestor(a,b) {
    while ((a = a.parentElement) && (a.tagName.toLowerCase() !== b.toLowerCase() && !a.classList.contains(b) && a.id !== b));
    return a;
}
Alpha attempt: where a is the element and b is the tag, class, or ID as a string.

Simple. Elegant. Works.

Now that I think on it, it would be just as easy to add in other means to look up a parent. One could easily add name attribute lookup, but I think I'll save that for a later time. I've yet to run into that... so far.

Tracy Parks

Published 8 months ago