Bring Your Own DOM – Part 1 – Portals

A banana traveling through a portal, from ThomasWolter

I’ve realized I’m not great at lengthy posts. Neither completing them, nor finding the right words.

Another thing I’ve come to realize over the past year is Virtual DOMs (vdom) have a sort of beauty in their simplicity. They generally have 2 basic actions: insert and remove. Combine this with the malleability of JavaScript and beautifully, simple magic can happen.

Portals

The code is here for those that like to jump to the end. Linking it in a gist because over the past year we’ve made 1 or 2 tiny adjustments and I’d rather give you the current working code than something outdated.

How do they work?

window.customElements.define("elm-portal", class extends HTMLElement {
  ...
}

We first define a custom element. Naming is hard, so why not follow the common naming of <domain>-portal.

constructor() {
  super();
  this._targetNode = document.createElement("div");
}

Our portal needs a target location to end up at, so we define one.

connectedCallback() {
  const selector = this.getAttribute("data-target-selector");
  const destination = document.querySelector(selector);
  destination.appendChild(this._targetNode);
}

When our element is added to the DOM, we want to make sure our target location is present. We declare how we’re going to find it with const selector = this.getAttribute("data-target-selector"), then reach out and grab it with const destination = document.querySelector(selector);, and finally place our target at the destination with destination.appendChild(this._targetNode);

disconnectedCallback() {
  const selector = this.getAttribute("data-target-selector");
  const destination = document.querySelector(selector);
  destination.removeChild(this._targetNode);
}

We don’t want to forget to remove our destination when our portal is removed, otherwise we could end up with wild clones of our self!


So far this has all been basic JavaScript custom element code. Now for the Elm specific code!

get childNodes() {
  return this._targetNode.childNodes;
}

Sometimes the Elm vdom needs to retrieve the list of child nodes, so we forward on that request.

replaceData(...args) {
  return this._targetNode.replaceData(...args);
}

Other times the Elm vdom asks to replace some data, so we forward on that request.

removeChild(...args) {
  return this._targetNode.removeChild(...args);
}

Elm’s vdom definitely needs to be able to remove a child, so we forward on that request. Noticing a pattern here?

insertBefore(...args) {
  return this._targetNode.insertBefore(...args);
}

Sometimes it’s asks to insert something before our portal, so we forward on that request too.

appendChild(...args) {
  // To cooperate with the Elm runtime
  requestAnimationFrame(() => {
    return this._targetNode.appendChild(...args);
  });
}

Finally Elm’s vdom will ask to append a child to the portal. So, as usual, we forward on that request.

That’s it?

Yep, that’s it. All that’s needed to have portals in Elm is to forward these 4 functions calls and 1 property request. Adding this to your project will allow you to define an element in 1 location and have it appear in another.

Do I need this?

Hopefully not for long. Today it’s very helpful for building custom dropdowns, tooltips, and anything else that needs to be “on top” no matter where it’s defined in your view code. Thankfully there’s a proposal for a popover API that would allow all of this functionality, and more, without the need for custom elements!

So then, why share this? Well this is just the tip of the ice berg. In part 2 we’ll see how this train of thought can take us far beyond the browser.

I also need to give credit to Ryan Haskell-Glatz for pairing with me on this for many hours. A good chunk of my first week at Vendr was figuring this out and it’s made our code a lot more clean and easy to use.

1 Comment

Leave a Comment