patternjavascriptMinor
Determine closest ancestor that has a certain HTML class
Viewed 0 times
hasthatancestordetermineclosestcertainclasshtml
Problem
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:
Code:
Notes:
Algorithm:
- 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
- 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 ofElement.prototype. Does this approach yield any problems? I am aware that I would override anygetAncestorByClassName()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,
nullis returned as other functions likegetElementById()do the same.
- Browser support should cover all modern browsers. I do not consider IE8–IE10 modern browsers. The lack of support for
classListon these is noted but not essential to me right now.
Solution
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:
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
As for the code:
The first step you take is to call
Your also doing some unnecessary work in checking for
And there's no reason to redeclare
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:
Of course, since you're extending the
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);
}Code Snippets
if (!Element.prototype.getAncestorByClassName) {
// extend the prototype
}Element.prototype.getAncestorByClassName = function(className) {
var currentElement = this;
while (currentElement) {
if (currentElement.classList.contains(className)) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
return null;
}Element.prototype.getAncestorByClassName = function(className) {
if (this.classList.contains(className)) {
return this;
}
if (!this.parentElement) {
return null;
}
return this.parentElement.getAncestorByClassName(className);
}Context
StackExchange Code Review Q#155314, answer score: 6
Revisions (0)
No revisions yet.