Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Creating a Modal Dialog Web Component

August 01, 2018
javascript

In the last post I debated whether standard web components are finally a thing. It certainly feels like they are starting to get real traction now. So, how do you build standard web components? In this post we’ll build a simple modal dialog component …

Modal dialog

Modal dialog specification

Our modal dialog will look a like above and the following markup will create it:

<x-modal title="Important!" visible>
  <p>This is some really important stuff</p>
</x-modal>

So, the our tag name is x-modal. A title attribute let’s us define a title for the dialog. The content inside the x-modal tag will be the content of the dialog.

A visible attribute will determine whether the dialog is open or closed.

This dialog will have “Okay” and “Cancel” buttons which will invoke events called “ok” and “cancel” respectively.

Our basic web component

In a html page let’s create the basic web component below in a script tag.

class Modal extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this._render();
  }

  _render() {
    const container = document.createElement("div");
    container.innerHTML = `
      <style>
        .wrapper {
          position: fixed;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
          background-color: gray;
          opacity: 0;
          visibility: hidden;
          transform: scale(1.1);
          transition: visibility 0s linear .25s,opacity .25s 0s,transform .25s;
          z-index: 1;
        }
        .visible {
          opacity: 1;
          visibility: visible;
          transform: scale(1);
          transition: visibility 0s linear 0s,opacity .25s 0s,transform .25s;
        }
        .modal {
          font-family: Helvetica;
          font-size: 14px;
          padding: 10px 10px 5px 10px;
          background-color: #fff;
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%,-50%);
          border-radius: 2px;
          min-width: 300px;
        }
        .title {
          font-size: 18px;
        }
        .button-container {
          text-align: right;
        }
        button {
          min-width: 80px;
          background-color: #848e97;
          border-color: #848e97;
          border-style: solid;
          border-radius: 2px;
          padding: 3px;
          color:white;
          cursor: pointer;
        }
        button:hover {
          background-color: #6c757d;
          border-color: #6c757d;
        }
      </style>
      <div class='wrapper visible'>
        <div class='modal'>
          <span class='title'>We need to add a title here</span>
          <div class='content'>
            We need to add some content here
          </div>
          <div class='button-container'>
            <button class='cancel'>Cancel</button>
            <button class='ok'>Okay</button>
          </div>
        </div>
      </div>`;

    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(container);
  }
}
window.customElements.define("x-modal", Modal);

We use window.customElements.define() to let the page know about our web component, passing in our chosen tag name and an ES6 class that gives the functionality of our component.

connectedCallback is a function that gets invoked when our component in loaded. In this function we put the html and CSS for our dialog in a big literal string in a div. We then put this div in the component’s shadow DOM.

So, if we reference our component with:

<x-modal></x-modal>

… we get:

Modal dialog step 1

Effect of the shadow DOM

Anything inside the shadow DOM of the component is protected and can’t be accessed from outside the component.

Modal dialog step 1

Styles are protected as well the internal elements.

This means that our component should never clash with other stuff on the page.

Neat!

Adding a title attribute

Let’s add a title attribute to our component:

class Modal extends HTMLElement {
  constructor() {
    super();
  }

  get title() {    return this.getAttribute("title");  }
  set title(value) {    this.setAttribute("title", value);  }}

… and reference this in the markup that is rendered:

_render() {
  const container = document.createElement("div");
  container.innerHTML = `
    <style>
      ...
    </style>
    <div class='wrapper visible'>
      <div class='modal'>
        <span class='title'>${this.title}</span>        <div class='content'>
          ...
        </div>
        <div class='button-container'>
          ...
        </div>
      </div>
    </div>`
 };

If we add a title attribute the markup that references in x-modal:

<x-modal title="Important!"></x-modal>

… we get:

Modal dialog step 2

“Slotting” the content

We can use a slot to allow custom content in our component. All we need to do is place a slot element where we want to allow custom content to be injected into our component.

_render() {
  const container = document.createElement("div");
  container.innerHTML = `
    <style>
      ...
    </style>
    <div class='wrapper visible'>
      <div class='modal'>
        <span class='title'>...</span>
        <div class='content'>
          <slot></slot>        </div>
        <div class='button-container'>
          ...
        </div>
      </div>
    </div>`
};

If we add some content to the markup that references x-modal:

This is some really important stuff

<x-modal title="Important!">
  <p>This is some really important stuff</p></x-modal>

… we get:

Modal dialog step 3

Adding a visible attribute

Let’s add a visible attribute to show and hide the dialog.

class Modal extends HTMLElement {
  get visible() {
    return this.hasAttribute("visible");
  }

  set visible(value) {
    if (value) {
      this.setAttribute("visible", "");
    } else {
      this.removeAttribute("visible");
    }
  }

  ...
}

… and reference this in the markup that is rendered:

_render() {
  const wrapperClass = this.visible ? "wrapper visible" : "wrapper";  const container = document.createElement("div");
  container.innerHTML = `
    <style>
      ...
    </style>
    <div class='${wrapperClass}'>      <div class='modal'>
        <span class='title'>...</span>
        <div class='content'>
          ...
        </div>
        <div class='button-container'>
          ...
        </div>
      </div>
    </div>`;
 }

Our dialog doesn’t show by default now. To make the the dialog show, we need to add the visible attribute to x-modal:

<x-modal title="Important!" visible>  <p>This is some really important stuff</p>
</x-modal>

Reacting to attribute changes

We would like to add / remove the visible attribute after the component is rendered to hide and show the dialog. In fact we want the “okay” and “cancel” buttons to remove this attribute to hide the dialog. So, how do you do this?

We can use attributeChangedCallback() which is a function that runs whenever an attribute changes. As well as this, we need to “observe” the attributes that we want to track changes to using observedAttributes.

Let’s handle title attribute changes as well as visible attribute changes.

When the title attribute changes, we want to update our title in our shadow DOM.

When the visible attribute changes, we want to add / remove the visible CSS class to the wrapper div.

class Modal extends HTMLElement {
  get visible() {...}

  set visible(value) {...}

  get title() {...}

  set title(value) {...}

  constructor() {...}

  connectedCallback() {...}

  static get observedAttributes() {    return ["visible", "title"];  }
  attributeChangedCallback(name, oldValue, newValue) {    if (name === "title" && this.shadowRoot) {      this.shadowRoot.querySelector(".title").textContent = newValue;    }    if (name === "visible" && this.shadowRoot) {      if (newValue === null) {        this.shadowRoot.querySelector(".wrapper").classList.remove("visible");      } else {        this.shadowRoot.querySelector(".wrapper").classList.add("visible");      }    }  }
  _render() {...}
}

visible attribute changing

Button handlers

The buttons don’t do anything yet. We simply want them to emit the “ok” / “cancel” event and close the dialog. So, let’s first attach event handlers for our buttons that add / remove the visible attribute:

connectedCallback() {
  this._render();
  this._attachEventHandlers();}

_attachEventHandlers() {  const cancelButton = this.shadowRoot.querySelector(".cancel");  cancelButton.addEventListener('click', e => {    // TODO - invoke "cancel event"    this.removeAttribute("visible");  });  const okButton = this.shadowRoot.querySelector(".ok");  okButton.addEventListener('click', e => {    // TODO - invoke "ok event"    this.removeAttribute("visible");  });}

Expose the events

So, how to we raise events from our component? We can use dispatchEvent():

_attachEventHandlers() {
  const cancelButton = this.shadowRoot.querySelector(".cancel");
  cancelButton.addEventListener('click', e => {
    this.dispatchEvent(new CustomEvent("cancel"))    this.removeAttribute("visible");
  });
  const okButton = this.shadowRoot.querySelector(".ok");
  okButton.addEventListener('click', e => {
    this.dispatchEvent(new CustomEvent("ok"))    this.removeAttribute("visible");
  });
}

If we subscribe to these events in the consuming code we should find that they are handled when the buttons are clicked:

<button class="open">Open</button>
<x-modal title="Important!" visible>
  <p>This is some really important stuff</p>
</x-modal>

<script>
  modal = document.querySelector("x-modal");
  modal.addEventListener("cancel", function() {
    console.log("cancel event raised");
  });
  modal.addEventListener("ok", function() {
    console.log("ok event raised");
  });

  open = document.querySelector(".open");
  open.addEventListener("click", function() {
    modal.visible = true;
  });
</script>

visible attribute changing

Wrap up

So, that’s it - a simple modal dialog using standards based web components using a custom element with a shadow DOM.

The full code is in github. Hope that you find this useful!


Comments

Raymund August 25, 2019

Great post! I’m so grateful to look into your article, not only saved me but the opportunity of understanding such functions or whatever we call it. (e.g. connectedCallback(), attributeChangedCallback(…,…,…) etc. I’m seeing them everyday for the past 3 weeks but don’t exactly comprehend to how use it. I’m an average developer and I had a hard time looking for a modal component to work right away by putting stuff together. But this one is a bomb! honestly man, Thank you.

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter