Not concerned about old browser fallback. Also, can't use libraries.
I have an event object. I am testing the event.target against a css selector via matchesSelector:
event['target'].matchesSelector('css selector here');
this works, as does:
event['target']['parentElement'].matchesSelector('css selector here');
...and:
event['target']['parentElement']['parentElement'].matchesSelector('css selector here');
What I'm looking for is some possible object method beyond my understanding that I could use to check each parentElement all the way up for a match, without a for
loop. My focus is on efficiency.
Thanks!
Not concerned about old browser fallback. Also, can't use libraries.
I have an event object. I am testing the event.target against a css selector via matchesSelector:
event['target'].matchesSelector('css selector here');
this works, as does:
event['target']['parentElement'].matchesSelector('css selector here');
...and:
event['target']['parentElement']['parentElement'].matchesSelector('css selector here');
What I'm looking for is some possible object method beyond my understanding that I could use to check each parentElement all the way up for a match, without a for
loop. My focus is on efficiency.
Thanks!
Share Improve this question edited Apr 11, 2022 at 22:24 John Slegers 47.1k23 gold badges204 silver badges173 bronze badges asked Oct 19, 2012 at 15:34 Randy HallRandy Hall 8,12721 gold badges84 silver badges170 bronze badges 4 |4 Answers
Reset to default 12The closest() function will do what you need.
It starts from the element itself, and traverses parents (heading toward the document root) until it finds a node that matches the provided selectorString. Somewhat similar to jQuery's parents()
function.
So your code will look like that:
event.target.closest(selectorString)
To prevent redundant looping through all parent elements of your target element, you can perform quick checking for whether your element is inside of an element that matches your selector by using matchesSelector()
with selector that is concatenation of your original selector and appended context selector consisting of space and your target-element's tag name:
function getAncestorBySelector(elem, selector) {
if (!elem.matchesSelector(selector + ' ' + elem.tagName)) {
// If element is not inside needed element, returning immediately.
return null;
}
// Loop for finding an ancestor element that matches your selector.
}
Marat Tanalin's solution is fine, but the standard syntax for elem.matchesSelector
is elem.matches
:
function getAncestorBySelector(elem, selector) {
if (!elem.matches(selector + ' ' + elem.tagName)) {
// If element is not inside needed element, returning immediately.
return null;
}
// Loop for finding an ancestor element that matches your selector.
}
Unfortunately, not all browsers support this syntax yet. Several browsers still implement a prefixed version of elem.matchesSelector
and Opera Mini doesn't support this feature at all.
To alleviate that issue, you could combine the approach described with the following polyfill, as suggested by the MDN :
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
Alternatively, you might want to consider throwing out elem.matchesSelector
entirely and go with something like this :
function getAncestorBySelector(elem, selector) {
if([].indexOf.call(document.querySelectorAll(selector + ' ' + elem.tagName), elem) === -1) {
// If element is not inside needed element, returning immediately.
return null;
}
// Loop for finding an ancestor element that matches your selector.
}
Both implementations require at least support for querySelectorAll
, which is a feature supported by all modern browsers.
Browser support :
Most questions that ask for a re-implementation of jQuery's parents()
method in vanilla-js are closed as duplicates of this question, I wanted to provide a more modern example that better emulates jQuery's behaviour.
Typescript:
function getAllParentElements(child: HTMLElement, selector: string = '*') {
const parents: HTMLElement[] = [];
let parent: HTMLElement = child.parentElement?.closest(selector);
while (parent) {
parents.push(parent);
parent = parent.parentElement?.closest(selector);
}
return parents;
}
Javascript:
function getAllParentElements(child, selector = '*') {
const parents = [];
let parent = child.parentElement && child.parentElement.closest(selector);
while (parent) {
parents.push(parent);
parent = parent.parentElement && parent.parentElement.closest(selector);
}
return parents;
}
getAllParentElements(childEl); // -> return all ancestors including body & html elements
getAllParentElements(childEl, '.foo'); // -> return all ancestors with `.foo` class
for
loop? Awhile
loop would also work, or possibly recursion, but I'm not sure why you want that. – pimvdb Commented Oct 19, 2012 at 15:37document.querySelectorAll('...')
and thenindexOf
on the node list would not requirematchesSelector
each time. But I doubt it will be faster.matchesSelector
is only defined on elements, not node lists, so you can't quite avoid a loop of some sort. – pimvdb Commented Oct 19, 2012 at 15:51