4

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!

John Slegers
  • 45,213
  • 22
  • 199
  • 169
Randy Hall
  • 7,716
  • 16
  • 73
  • 151
  • Just a thought... perhaps "flatten" the event.target's parents into some sort of object that could be queried against? – Randy Hall Oct 19 '12 at 15:36
  • 2
    Why are you avoiding a `for` loop? A `while` loop would also work, or possibly recursion, but I'm not sure why you want that. – pimvdb Oct 19 '12 at 15:37
  • 1
    @pimvdb For absolute best efficiency, a "browser-native" way to access and match against the event object properties all at once – Randy Hall Oct 19 '12 at 15:39
  • Technically, using `document.querySelectorAll('...')` and then `indexOf` on the node list would not require `matchesSelector` 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 Oct 19 '12 at 15:51

4 Answers4

9

The 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)
Bassem
  • 2,736
  • 31
  • 13
5

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
  • 13,927
  • 1
  • 36
  • 52
  • 1
    A suggestion to rather use elem.matches and make it cross browser compliant using the following Polyfill: https://developer.mozilla.org/en/docs/Web/API/Element/matches – Pancho Jul 28 '16 at 16:22
4

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 :

enter image description here

Community
  • 1
  • 1
John Slegers
  • 45,213
  • 22
  • 199
  • 169
  • 1
    Just a note that element.matches can easily be made cross browser compliant using the Mozilla provided Polyfill. see my comment above – Pancho Jul 28 '16 at 16:29
  • @Pancho : That's a reasonable approach indeed. I updated my answer to include your suggestion as an alternative implementation. – John Slegers Jul 28 '16 at 16:52
2

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
JaredMcAteer
  • 21,688
  • 5
  • 49
  • 65