I am creating a dropdown menu web component that will be used by consumers like:
<custom-menu>
<custom-menu-anchor>
<button>Toggle Menu</button>
</custom-menu-anchor>
<custom-menu-item>Fish</custom-menu-item>
<custom-menu-item>
<custom-icon name="chicken"/>
<span>Chicken</span>
</custom-menu-item>
</custom-menu>
Here, the slot items like <custom-menu-items>
need to be absolutely positioned.
To do this, we need to
To setup a perfect overlay, I need to create an overlay/surface
element and remove the custom-menu-item
children and append them all to the overlay element.
To achieve this, I attempted something like below in connectedCallback
lifecycle method:
const slot = this.shadowRoot.querySelector('slot');
const surface = document.createElement('div');
const nodes = slot.assignedNodes();
surface.append(...nodes);
document.body.appendChild(surface);
Problems with this approach:
assignedNodes
from slot messes up many things.slotchange
doesn't work once the elements are moved. lit-html
, Vue
or even plain JavaScript
. I noticed that this starts breaking abstractions of the consumer libraries after moving these DOM elements.This need applies to any absolutely positioned/offset components like notification, dialog, dropdown, Snackbar and the above approach clearly struggles and is certainly not a cleaner way to do things.
How can we do this in a more effective manner avoiding all the mentioned side effects ?
Moving light dom nodes around is usually not a good idea - your users (and the browser) expects them to stay where they are.
If you want to render the content somewhere else you could provide the content as an attribute.
A simplified example for lit-html (using it to set a property via .
)
<my-el .content=${'<div>my content</div>'}></my-el>
Then in your custom element, you will need to do something with that string which is not yet in the dom.
class MyEl extends ... {
onContentChanged() {
document.body.querySelector('my-el-target').innerHTML = this.content;
}
}
PS: this is highly oversimplified - in real you probably want to create/manage that my-el-target on connectedCallback - or let it be handled by a full controller. Also, the "template" probably should be a lit-template instead of a plain string...