Custom Element getRootNode.closest() function crossing multiple (parent) shadowDOM boundaries
Asked Answered
C

4

12

I spent some time searching but have only seen too many regular "walk the DOM" blogs or answers that only go one level UP with getRootnode()

Pseudo code:

HTML

<element-x>
//# shadow-root
    <element-y>
        <element-z>
        //# shadow-root
        let container = this.closest('element-x');
        </element-z>
    </element-y>
</element-x>

The standard element.closest() function does not pierce shadow boundaries;

So this.closest('element-x') returns null because there is no <element-x> within <element-z> shadowDom

Goal:

Find <element-x> from inside descendant <element z> (any nested level)

Required:

A (recursive) .closest() function that walks up the (shadow) DOMs and finds <element-x>

Note: elements may or may not have ShadowDOM (see <element y>: only lightDOM)

I can and will do it myself tomorrow; just wondered if some bright mind had already done it.

Resources:

Update

This is the UNminified code from the answer below:

        closestElement(selector, base = this) {
            function __closestFrom(el) {
                if (!el || el === document || el === window) return null;
                let found = el.closest(selector);
                if (found)
                  return found;
                else
                  __closestFrom(el.getRootNode().host);
            }

            return __closestFrom(base);
        }

Update #2

I changed it to a method on my BaseElement:

  closestElement(selector, el = this) {
    return (
      (el && el != document && el != window && el.closest(selector)) ||
      this.closestElement(selector, el.getRootNode().host)
    );
  }

Events

As Intervalia comments; yes Events are another solution.
But then... an Event needs to be attached to an ancestor... How to know which ancestor to use?

Cloying answered 4/2, 2019 at 16:38 Comment(1)
I wonder if this could not be better handled by events. As a general rule it is not good for a child to know anything about a parent and especially about a grandparent. That is the role of events, to allow the ancestors to obtain information about a descendant. Then the parents, since they created the children would sent properties or call functions on the child. I am curious the goal you are after in trying to walk up through the ancestor list.Photo
J
7

This does the same as .closest() from inside any child (shadow)DOM

but walking up the DOM crossing shadowroot Boundaries

Optimized for (extreme) minification

//declared as method on a Custom Element:
closestElement(
    selector,      // selector like in .closest()
    base = this,   // extra functionality to skip a parent
    __Closest = (el, found = el && el.closest(selector)) => 
        !el || el === document || el === window
            ? null // standard .closest() returns null for non-found selectors also
            : found 
                ? found // found a selector INside this element
                : __Closest(el.getRootNode().host) // recursion!! break out to parent DOM
) {
    return __Closest(base);
}

Note: the __Closest function is declared as 'parameter' to avoid an extra let declaration... better for minification, and keeps your IDE from complaining

Called from inside a Custom Element:

<element-x>
//# shadow-root
    <element-y>
        <element-z>
        //# shadow-root
        let container = this.closestElement('element-x');
        </element-z>
    </element-y>
</element-x>
Jozef answered 5/2, 2019 at 11:1 Comment(1)
@Danny Your suggested edit was incorrect. The answer states “//declared as method on a Custom Element:”. An arrow function would be incorrect here.Animadvert
G
8

Something like this should do the trick

function closestPassShadow(node, selector) {

    if (!node) {
        return null;
    }

    if (node instanceof ShadowRoot) {
        return this.closestPassShadow(node.host, selector);
    }

    if (node instanceof HTMLElement) {
        if (node.matches(selector)) {
            return node;
        } else {
            return this.closestPassShadow(node.parentNode, selector);
        }
    }

    return this.closestPassShadow(node.parentNode, selector);

}
Greenebaum answered 24/5, 2021 at 17:47 Comment(1)
Easy to read and does its job.Starks
J
7

This does the same as .closest() from inside any child (shadow)DOM

but walking up the DOM crossing shadowroot Boundaries

Optimized for (extreme) minification

//declared as method on a Custom Element:
closestElement(
    selector,      // selector like in .closest()
    base = this,   // extra functionality to skip a parent
    __Closest = (el, found = el && el.closest(selector)) => 
        !el || el === document || el === window
            ? null // standard .closest() returns null for non-found selectors also
            : found 
                ? found // found a selector INside this element
                : __Closest(el.getRootNode().host) // recursion!! break out to parent DOM
) {
    return __Closest(base);
}

Note: the __Closest function is declared as 'parameter' to avoid an extra let declaration... better for minification, and keeps your IDE from complaining

Called from inside a Custom Element:

<element-x>
//# shadow-root
    <element-y>
        <element-z>
        //# shadow-root
        let container = this.closestElement('element-x');
        </element-z>
    </element-y>
</element-x>
Jozef answered 5/2, 2019 at 11:1 Comment(1)
@Danny Your suggested edit was incorrect. The answer states “//declared as method on a Custom Element:”. An arrow function would be incorrect here.Animadvert
K
7

Excellent examples! Wanted to contribute a TypeScript version that has a minor difference -- it follows assignedSlot while traversing up the shadow roots, so you can find the closest matching element in a chain of nested, slotted custom elements. It's not the fanciest way to write the TypeScript, but it gets the job done.

Edit: This code was written 5 years ago and typescript itself has come a long way since then. A lot of JS code that used to require special handling should now “just work” in TypeScript. This code can and likely should be simplified. Use it, sure, but I’m not as proud of it today as I might have been 5 years ago :) I lack the time to edit it, however.

closestElement(selector: string, base: Element = this) {
  function __closestFrom(el: Element | Window | Document): Element {
    if (!el || el === document || el === window) return null;
    if ((el as Slotable).assignedSlot) el = (el as Slotable).assignedSlot;
    let found = (el as Element).closest(selector);
    return found
      ? found
      : __closestFrom(((el as Element).getRootNode() as ShadowRoot).host);
  }
  return __closestFrom(base);
}

The equvalent in JS is:

closestElement(selector, base = this) {
    function __closestFrom(el) {
        if (!el || el === document || el === window)
            return null;
        if (el.assignedSlot)
            el = el.assignedSlot;
        let found = el.closest(selector);
        return found
            ? found
            : __closestFrom(el.getRootNode().host);
    }
    return __closestFrom(base);
}
Kyungkyushu answered 13/5, 2019 at 3:24 Comment(0)
H
0

just a to endolge legibility / code style. this should be typescript friendly as well.

const closestElement = (selector, target) => {
  const found = target.closest(selector);

  if (found) {
    return found;
  }

  const root = target.getRootNode();

  if (root === document || !(root instanceof ShadowRoot)) {
    return null;
  }

  return closestElement(selector, root.host);
};
Harper answered 25/12, 2022 at 21:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.