0

In my real world case, I'm dealing with a flyout-component, which I want to close once the any element outside of it receives focus. This flyout-component is used as part of the shadow-dom template of other elements. And of course, my real world case, there are a lot more components involved. I reduced the case to a bare minimum.

In the following example, we have an outer and an inner-component. The inner one is little more than a plain slot and a focusout-listener as a demonstrator for my problem. The outer-component contains an unnamed slot and an inner-component, which contains a named slot.

So the inner-component will select the slot outer-nested-slot from the outer-component with its inner-plain-slot. Finally, it will contain the div with the two buttons.

Now here comes the question itself: The buttons are no real children of inner-component. Nevertheless the focusout-event is received since they are slotted into inner-component. Is there a way to programmatically check if a Element is logically a child of another component, even though there is no parent-child-relation in the light-dom?

Of course slot.assignedNodes({flatten:true}) came into my mint. But this would return only the wrapping div around my buttons. Thus I would have to iterate over all the returned nodes in order to check if any of them contains the element in question. And of course, if the assigned node was not a simple div, but again a webcomponent, everything gets ridiculous complex.

So, in a nutshell: Given a webcomponent w-a and a node b, is there a way to programmatically check if there is a logical parent-child-relation (implying an event could bubble from b to w-a )?

const stamp = (node, name) => {
  const template = document.getElementById(name);
  const templateContent = template.content;

  const shadowRoot = node.attachShadow({
    mode: 'open'
  });
  shadowRoot.appendChild(templateContent.cloneNode(true));
}


customElements.define('inner-element',
  class extends HTMLElement {
    constructor() {
      super();
      stamp(this, 'inner-element');

      this.addEventListener('focusout', this.onFocusOut);
    }

    onFocusOut(focusevent) {
      const newFocus = focusevent.relatedTarget;
      const oldFocus = focusevent.target;
      document.getElementById('log').innerHTML += `<div>${oldFocus?oldFocus.id:'null'} -> ${newFocus?newFocus.id:'null'}; oldFocus is Child? ${this.contains(oldFocus)}. newFocus is Child? ${this.contains(newFocus)}</div>`;
    }

  }
);

customElements.define('outer-element',
  class extends HTMLElement {
    constructor() {
      super();
      stamp(this, 'outer-element');
    }
  }
);
<template id="inner-element">
  <style>
    :host {
      border: 2px solid hotpink;
      margin:2px;
      padding: 2px;
    }
  </style>
  <slot id="inner-plain-slot"></slot>
</template>

<template id="outer-element">
  <style>
    :host {
      border: 2px solid rebeccapurple;
      margin:2px;
      padding: 2px;
      display:flex;
      flex-direction:row
    }
  </style>
  <inner-element>  <slot id="outer-nested-slot" name="nested"></slot>  </inner-element>
  <slot id="outer-plain-slot"></slot>
</template>


<outer-element>
  <div style="display:flex;flex-direction:row" slot="nested">
    <button id="nest1">nested-1</button>
    <button id="nest2">nested-2</button>
  </div>
  <button id="not-nest1">not-nested1</button>
  <button id="not-nest2">not-nested2</button>
</outer-element>

<div>
  <code id=log></code>
</div>

I found one possible solution using events themself. The solution seems rather obvious after condensing the question. But I would still prefer a more native way to perform that check.

const checkChildParentRelation = (potentialChild, potentialParent) => {
  if (!potentialChild || !potentialParent) {
    return false;
  }
  let result = false;
  const listener = e => {
    result = true;
    e.stopImmediatePropagation();
  };

  const eventName = `parent-child-test-event-${Date.now()}`;

  potentialParent.addEventListener(eventName, listener);
  potentialChild.dispatchEvent(
    new CustomEvent(eventName, {
      bubbles: true,
      composed: true,
      cancelable: true,
    })
  );
  potentialParent.removeEventListener(eventName, listener);
  return result;
};
samjaf
  • 911
  • 1
  • 7
  • 18
  • Did you try `parentElement.contains(childElement)` ? It might not work as expected with Shadow DOM but is worth trying ... – IVO GELOV Apr 14 '22 at 05:56
  • `contains` is used in the example. It was the first thing I tried in my real world case. And it is not working indeed. – samjaf Apr 14 '22 at 06:26

1 Answers1

-1

For loose coupling Events are best.

Sounds to me like you want to proces the array of DOM Elements you get from executing the event.composedPath() function

Danny '365CSI' Engelman
  • 11,626
  • 1
  • 22
  • 35
  • Sorry, this question is neither about coupling nor about the path of the event as such. All I want is to check wether a component is logically a child of another component, even though there is no parent-child relation in respect to light dom. Please have a closer look into my example. – samjaf Apr 15 '22 at 13:31
  • If ``b`` is a child of ``w-a``, then ``w-a`` is in the ``composedPath`` Array, when you dispatch an Event from ``b``, that is _loose_ coupling. You can do it with a [recursive ``closestElement``](https://stackoverflow.com/questions/54520554/custom-element-getrootnode-closest-function-crossing-multiple-parent-shadowd), but that would _tight_ coupling – Danny '365CSI' Engelman Apr 15 '22 at 15:15
  • Thank you for your input. As you can see in my addition, I already found a solution using events. There is no need for me to inspect the composed path. If I can receive the event, the question is answered already. I am looking for a more straight forward way to verify (or falsify) the relation. I was hoping for a native API. But the more I investigate, the less I'm confident in finding such an API. – samjaf Apr 15 '22 at 19:03