3

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

  1. Create an overlay
  2. Remove slot elements from web component
  3. Attach them to the overlay element.
  4. Provide correct positioning

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:

  • I noticed that removing the assignedNodes from slot messes up many things.
  • Many things do not work when I attempt to move slot's lightDOM
  • slotchange doesn't work once the elements are moved.
  • Further, web component could be used in any framework, It could be 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 ?

Krantisinh
  • 1,579
  • 13
  • 16
Harshal Patil
  • 17,838
  • 14
  • 60
  • 126

1 Answers1

1

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

daKmoR
  • 1,774
  • 12
  • 24
  • Looks like that is the only way. Slots are practically useless if you ever wish to move light DOM. – Harshal Patil Jun 20 '19 at 02:56
  • 1
    indeed slots have a different purpose - their only goal is to "slot" them into the direct parents shadow root. If you wish to actually move element you can do so with plain javascript `document.querySelector('#target').appendChild(elementToMove)` – daKmoR Jun 20 '19 at 14:58