javascriptshadow-domcustom-elementnative-web-component

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


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?


Solution

  • 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>