9
\$\begingroup\$

When writing JavaScript code for my own projects, this functionality is the singlemost used algorithm that does not already exist: Finding an ancestor element that has a certain HTML class.

Algorithm:

  1. Does the current element have a parent element?
    • Yes: Continue with step 2
    • No: There are no solutions to the task, return a null value
  2. Does the parent element of the current element have the required class?
    • Yes: Return the parent element
    • No: Set the current element to its parent element and repeat step 1

Code:

Element.prototype.getAncestorByClassName = function(className) {
    var currentParent = this.parentElement
    while (true) {
        // If the root of the DOM is reached, an ancestor cannot be found anymore.
        if (currentParent === null) {
            return null
        }

        if (currentParent.classList.contains(className)) {
            return currentParent
        }

        var currentParent = currentParent.parentElement
    }
}

Notes:

  • I want to be able to call element.getAncestorByClassName("class-name") hence the use of Element.prototype. Does this approach yield any problems? I am aware that I would override any getAncestorByClassName() functions that are introduced in future versions of ECMAScript
  • There are possibly multiple results for the task I specified. The algorithm will return the closest ancestor that has the given class.
  • If there are no results, null is returned as other functions like getElementById() do the same.
  • Browser support should cover all modern browsers. I do not consider IE8–IE10 modern browsers. The lack of support for classList on these is noted but not essential to me right now.
\$\endgroup\$
  • 5
    \$\begingroup\$ You should totally drop this and try jQuery. Wait…am I on the wrong site for that meme? \$\endgroup\$ – Cody Gray Feb 14 '17 at 15:58
  • 2
    \$\begingroup\$ Why not simply use the modern .closest('.classname') and polyfill it for the older browsers? \$\endgroup\$ – wOxxOm Feb 14 '17 at 22:06
  • 1
    \$\begingroup\$ @CodyGray jQuery does all things, including this site. \$\endgroup\$ – jpmc26 Feb 14 '17 at 23:26
6
\$\begingroup\$

Extending native prototypes is a little iffy. Not that this should cause any problems, but in principle it's best to avoid. The reason is mostly that if everyone extend native prototypes, then you're more likely to run into issues (i.e. if you include a library that tramples on your extensions or vice-versa). Hence the general consensus is to try to leave them alone.

All that said, the main point approach it with open eyes, and proceed at your own risk.

In this case, I doubt you risk much, but it's always a good idea to consider something like:

if (!Element.prototype.getAncestorByClassName) {
  // extend the prototype
}

just to only do the extension if the function doesn't already exist. Of course, this is considerably more useful for polyfills, where a standardised function does exist and its signature is known. Here, if a getAncestorByClassName function already exists, it's hard to know if it works the same as yours since either one would be non-standard. Anyway, just something to consider.

As for the code:

The first step you take is to call currentNode.parentElement. This precludes you finding the element itself. Granted the function is called get ancestor, but still... say you call it on an element like body > p > span.highlight, and ask it to find the class highlight: You'd get null back. I'd include the initial element in the search.

Your also doing some unnecessary work in checking for === null inside the loop, since the while loop's condition could do that. Since you always end up exiting the loop somehow, it might as well be apparent in the loop's own condition, rather than have it be explicitly defined as an infinite loop.

And there's no reason to redeclare var currentNode inside the loop; it's already declared.

Also not a fan of the "minimal semicolons"-style, but that's just me. Works without them too.

Cleaned up a bit, it could look like:

Element.prototype.getAncestorByClassName = function(className) {
    var currentElement = this;

    while (currentElement) {
        if (currentElement.classList.contains(className)) {
            return currentElement;
        }
        currentElement = currentElement.parentElement;
    }

    return null;
}

Of course, since you're extending the Element prototype you could be clever and make it recursive. It looks nice, but I'd probably stick to the simpler loop myself:

Element.prototype.getAncestorByClassName = function(className) {
    if (this.classList.contains(className)) {
        return this;
    }

    if (!this.parentElement) {
        return null;
    }

    return this.parentElement.getAncestorByClassName(className);
}
\$\endgroup\$
  • \$\begingroup\$ Leaving out the element itself was intentional. How to go about this depends largely on the usecase. The 2nd var statement for currentElement is of course superfluous. \$\endgroup\$ – kleinfreund Feb 14 '17 at 13:46
  • \$\begingroup\$ I’d probably be more explicit about checking anything other than boolean variables like in while (currentElement) since this can cause quite some confusion for beginners. \$\endgroup\$ – kleinfreund Feb 14 '17 at 13:48
  • \$\begingroup\$ Out of curiosity, do you have an argument against dropping superfluous semi-cola in JavaScript? \$\endgroup\$ – kleinfreund Feb 14 '17 at 13:50
  • 1
    \$\begingroup\$ If you don't want to match the original element, then you could use a while ... do loop instead of a normal while loop. \$\endgroup\$ – RoToRa Feb 14 '17 at 14:27
  • 2
    \$\begingroup\$ I actually like the recursive approach here. The seems more logically aligned for use in a tree hierarchy. \$\endgroup\$ – Mike Brant Feb 14 '17 at 15:18
1
\$\begingroup\$

Adding on Flambino's answer, I just have to say that your code breaks on IE9.
Since you didn't specify which browsers you want to support and which versions, I will assume you want to support all modern browsers plus IE8.

According to the MDN page about Element.classList, this method is only supported on IE10+.
Currently, Windows Vista SP2 is supported (until 11th April), which only supports IE9.
IE9 can still be installed on Windows 7, and may be the default browser for some.

Also, devices with Android 2.1 will throw errors, since this isn't supported.


How to "fix" it?

  1. Add a polyfill for Element.classList
  2. Change your verification to verify if the class is on the property className.

If you want to support older browsers, this is required.
Otherwise, just ignore this, since all modern browsers support it. (Partially or completelly)

\$\endgroup\$
  • \$\begingroup\$ I agree with your assumption that I want to support all modern browser, however IE8–IE10 are not modern in my book. \$\endgroup\$ – kleinfreund Feb 14 '17 at 18:48
  • \$\begingroup\$ @kleinfreund IE10 can be considered dead. You will find IE9 or IE11. IE10 is only available on Windows 8 (unsupported) or Windows 7 (if you don't update your computer for a few years). \$\endgroup\$ – Ismael Miguel Feb 14 '17 at 22:52
  • \$\begingroup\$ And the usage of IE9 never was high. Today, my sites get any visits from IE9 or IE10. \$\endgroup\$ – kleinfreund Feb 16 '17 at 9:38
  • 1
    \$\begingroup\$ @kleinfreund If you get, you kinda want to be ready for it. It's not like you have to write 3000+ lines of Javascript. You can use something like (new RegExp('(?:^|\s)' + className.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') + '(\s|$)')).test(currentElement.className) to check if the class exists in the element, only using it if classList doesn't exist. \$\endgroup\$ – Ismael Miguel Feb 16 '17 at 10:42
  • \$\begingroup\$ I’m completely fine if one of my sites breaks on old browsers. \$\endgroup\$ – kleinfreund Feb 16 '17 at 10:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.