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;
};