Aha! Develop is for healthy enterprise development teams — that use scrum, kanban, and SAFe frameworks


Learn more
2021-05-28

Web components and implicit slot names

Since the dawn of the internet, web developers have had an unfulfilled desire. We've wanted "living elements" that can automatically react to state changes and user input. We've wanted something with the expressive and reactive power of Javascript, the simplicity and ease of use of HTML, and we've wanted to have it all packaged up in a neat, reusable component.

The introduction and standardization of web components gave HTML this superpower. Developers are now able to create native, interactive, performant, and easily reusable components using only plain HTML and Javascript. Because web components are based on native browser technologies, they can be used in any site, anywhere, without requiring a large technological investment, or a large rewrite of existing code. They also play nicely with other web frameworks, and so can be used inside plain HTML documents as as well as React, Vue, Svelte, Angular, etc. components with very few issues. For technology companies like Aha!, the value add is simply too alluring to ignore.

Even though web components are a browser-native technology ... or perhaps because of it ... their interface often leaves much to be desired. Indeed, dozens of different web component frameworks exist expressly for the purpose of removing some of the more tedious details of using web components.

We'd like to share a convention that we recently started using with our web components which helps smooth over a particular ugly bit of the web component interface. We call it "implicit slot names", and it's a convention that is framework agnostic and can be used no matter how you build web components.

Background

One of the most powerful aspects of web components is the "Shadow DOM." The shadow DOM allows developers to specifically target parts of the "Light DOM" (the elements a user sees on the page) and transport them within a different DOM tree that only exists inside the web component itself. This enables developers to make arbitrarily complex components which theoretically have a very simple public interface, by allowing slotted content to populate specific areas of our web component.

Take, for example, a web component for a modal window. A modal window typically has three specific areas within it — a header, a body, and a footer.

Modal window sections

We discovered that the sweet spot for web components is when we build fairly simple components that can be composed together to make more complex things. This allows us to cleanly separate the concerns of one component from that of another. To this end, we built four different components to create a full modal window — namely a modal-window, a modal-header, a modal-body, and a modal-footer.

Here is a simple example to illustrate how and why we build implicit slot names into our web components, as well as the benefits they provide:

// ----- JAVASCRIPT -----
class ModalWindow extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          display: inline-flex;
          flex-direction: column;
          border: 1px solid gray;
          box-shadow: 0 0.8rem 0.8rem gray;
        }
      </style>
      <slot name="header"></slot>
      <slot name="body"></slot>
      <slot name="footer"></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

class ModalHeader extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          background: #ddd;
          border-bottom: 1px solid gray;
          padding: 0.5rem;
        }
      </style>
      <slot></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

class ModalBody extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          background: #fff;
          padding: 0.5rem;
        }
      </style>
      <slot></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

class ModalFooter extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          background: #ddd;
          border-top: 1px solid gray;
          padding: 0.5rem;
        }
      </style>
      <slot></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

customElements.define("modal-window", ModalWindow);
customElements.define("modal-header", ModalHeader);
customElements.define("modal-body", ModalBody);
customElements.define("modal-footer", ModalFooter);

When using our web component within our page, we'd likely write something similar to this:

<!-- HTML -->
<modal-window>
  <modal-header slot="header">Header stuff</modal-header>
  <modal-body slot="body">Body stuff</modal-body>
  <modal-footer slot="footer">Footer stuff</modal-footer>
</modal-window>

which in turn would render as:

Rendered modal window component

So what's the problem? Everything works, isn't that enough?

The problem

The particular issue we have with the above example is that not all of our web components are made for internal use.

Recently, we launched Aha! Develop which allows our users to create and use custom components that they develop within their accounts, greatly extending the capabilities of Aha! Develop to encompass almost anything a customer could desire.

With this power came a problem, however.

Because people using Aha! Develop are building their own components, which they would want to seamlessly blend with the rest of the application's interface, they are using web component building blocks that we created and adding functionality to them. So our users would be building their own custom web components using things like <modal-window> above. And when exposing internal tools to the public at large you learn a lesson quickly: the interface matters.

<!-- HTML -->
<modal-window>
  <modal-header slot="header">Header stuff</modal-header>
  <modal-body slot="body">Body stuff</modal-body>
  <modal-footer slot="footer">Footer stuff</modal-footer>
</modal-window>

For a developer who is familiar with web components and who is building a modal window, it's tedious and error prone to have to remember that the <modal-header> component has to be slotted into the header slot within the parent <modal-window> component. Needing to understand that level of detail about how things get constructed in <modal-window> is a bit of a code smell too. Developers shouldn't need to understand how a component is made in order to use that component.

Worse, if the person developing the Aha! Develop extension is unfamiliar with web components, then they likely will not understand the need for the slot declaration at all. Sure, we could point them to helpful MDN articles and let them figure it out for themselves, but why should we make them absorb all that context to use something that looks like simple HTML tags when we can do more to make their lives easier?

Ideally, a <modal-header> would just know that it belongs at the top of a <modal-window>, just like <modal-footer> would know it belongs at the bottom. We want a clean and simple public interface like the following:

<!-- HTML -->
<modal-window>
  <modal-header>Header stuff</modal-header>
  <modal-body>Body stuff</modal-body>
  <modal-footer>Footer stuff</modal-footer>
</modal-window>

How do we accomplish that?

The solution

After some thought, it occurred to us that the issue with the slot declaration in web components is that while it tells us where the contents of a slot get appended, it doesn't tell us anything about what that content should be.

But what if it did? Things like a <modal-header> component really only ever make sense inside a <modal-window> component, so why don't they just automatically tell the parent <modal-window> where they belong?

// ----- JAVASCRIPT -----
class ModalHeader extends HTMLElement {
  constructor() {
    ...
    this.slot = this.hasAttribute('slot') ? this.slot : "header";
    ...
  }
}

class ModalBody extends HTMLElement {
  constructor() {
    ...
    this.slot =  this.hasAttribute('slot') ? this.slot : "body";
    ...
  }
}

class ModalFooter extends HTMLElement {
  constructor() {
    ...
    this.slot =  this.hasAttribute('slot') ? this.slot : "footer";
    ...
  }
}
<!-- HTML -->
<modal-window>
  <modal-header>Header stuff</modal-header>
  <modal-body>Body stuff</modal-body>
  <modal-footer>Footer stuff</modal-footer>
</modal-window>

In each individual component we check for the presence of a user-defined slot value first to ensure that the user-defined slot value is is always respected. If they've left it undefined we automatically default to a value which says where we want this component to appear in its parent component.

By adding this simple line, any <modal-header>, <modal-body>, or <modal-footer> that is added to a page will automatically slot itself into the correct slot within its parent <modal-window>.

As a bonus, any future component that uses one of these components as its base class will also get slotted into the correct slot within the parent <modal-window>.

// ----- JAVASCRIPT -----
class FancyModalHeader extends ModalHeader {
  ...
}
<!-- HTML -->
<modal-window>
  <fancy-modal-header>Fancy header stuff</fancy-modal-header>
  <modal-body>Body stuff</modal-body>
  <modal-footer>Footer stuff</modal-footer>
</modal-window>

Potential "gotchas"

Because our web components now automatically define a slot in which they expect to exist, if you ever need a component to show in the default slot you'll have to explicitly specify it via <modal-body slot="">. While a bit unusual, it's unlikely that a developer will intentionally want a specific component to exist somewhere that you did not intend, so we feel the extra pain is justified.

Closing thoughts

While this is a pretty simple technique, we feel it is a nice improvement in the elegance of the "API" we can present with our web components.

We are using web components to give extension developers a way to match the look and feel of the rest of Aha! Develop.. Learn more about Aha! Develop and how you can request access so your team can start using it: https://www.aha.io/develop/overview

Final code

// ----- JAVASCRIPT -----
class ModalWindow extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          display: inline-flex;
          flex-direction: column;
          border: 1px solid gray;
          box-shadow: 0 0.8rem 0.8rem gray;
        }
      </style>
      <slot name="header"></slot>
      <slot name="body"></slot>
      <slot name="footer"></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

class ModalHeader extends HTMLElement {
  constructor() {
    super();
    this.slot =  this.hasAttribute('slot') ? this.slot : "header";
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          background: #ddd;
          border-bottom: 1px solid gray;
          padding: 0.5rem;
        }
      </style>
      <slot></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

class ModalBody extends HTMLElement {
  constructor() {
    super();
    this.slot =  this.hasAttribute('slot') ? this.slot : "body";
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          background: #fff;
          padding: 0.5rem;
        }
      </style>
      <slot></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

class ModalFooter extends HTMLElement {
  constructor() {
    super();
    this.slot =  this.hasAttribute('slot') ? this.slot : "footer";
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host {
          background: #ddd;
          border-top: 1px solid gray;
          padding: 0.5rem;
        }
      </style>
      <slot></slot>
    `;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

customElements.define("modal-window", ModalWindow);
customElements.define("modal-header", ModalHeader);
customElements.define("modal-body", ModalBody);
customElements.define("modal-footer", ModalFooter);
<!-- HTML -->
<modal-window>
  <modal-header>Header stuff</modal-header>
  <modal-body>Body stuff</modal-body>
  <modal-footer>Footer stuff</modal-footer>
</modal-window>
Nathan Wright

Nathan Wright

Nathan is a tech generalist who thrives in the front end. He is a senior software engineer at Aha! — the world’s #1 product development software. Previously, he worked on both the GoToManage and GoToMeeting platforms at Citrix.

Build what matters. Try Aha! free for 30 days.

Follow Aha!

Follow Nathan