Creating web component in javascript

Author
April 06, 2022

In this article iam gonna explain you how you can create custom web component in javascript i will demonstrate you popup modal example for this Web component is html element which you can configure as you want behind the scene it uses native html element

Why web component

  • Encapsulate Logic + UI

  • Reusable across pages

Tools for web component or custom html element

  • Custom HTML Element: Register your own html tag

  • Shadow Dom: Manage a separate DOM node tree for your html element

  • Template and Slots: Write Html templates that you can add to your html element

_ Ways for creating custom elements _

  • Autonomous Elements: These are the custom html element which doesn’t depend upon anything means not on native html element

  • Extended Built-In elements: You can create custom element by extending built in html element

Web component lifecycle

  • constructor() -> Baic initializations, Element created

  • connectedCallback() -> DOM Initializations, Element attached to dom

  • disconnectedCallback() -> Cleanup work, Element detached from dom

  • attributeChangedCallback() -> Observed Attribute updated, Update Data + DOM

Step 1 Create a folder and files for our modal component

Create a folder with the name it’s depend upon you and create two file named modal.js and index.html again the name of javascript file depends upon you

  • Inside modal.js file create a class for the web component and extend the class with HTMLElement and register your custom html element
class Modal extends HTMLElement {}

//registering custom element named as uc-modal
customElement.define("uc-modal", Modal);
  • Go to index.html file and call the modal.js file and our custom html element which we have registered above
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Web Components</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        margin: 2rem;
        font-family: sans-serif;
      }
    </style>
    <script src="modal.js"></script>
  </head>
  <body>
    <uc-modal></uc-modal>
  </body>
</html>

Step 2 Adding templates for our Web component using Shadow Dom

As i have explained in the begining of this article that shadow dom manage dom tree for our html element so that it doesn’t interfare with the dom tree outside our web component

And we need to update our web component code to the following

class Modal extends HTMLElement {
  constructor() {
    super();
    //registering the shadow dom
    this.attachShadow({ mode: "open" });
    this.isOpen = false;

    //template for our modal with slot to take content from outside this web component
    this.shadowRoot.innerHTML = `
        <style>
            #backdrop {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100vh;
                background: rgba(0,0,0,0.75);
                z-index: 10;
                opacity: 0;
                pointer-events: none;
            }

            :host([opened]) #backdrop,
            :host([opened]) #modal {
                opacity: 1;
                pointer-events: all;
            }

            :host([opened]) #modal {
                top: 15vh;
            }

            #modal {
                position: fixed;
                top: 10vh;
                left: 25%;
                width: 50%;
                z-index: 100;
                background: white;
                border-radius: 3px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.26);
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                opacity: 0;
                pointer-events: none;
                transition: all 0.3s ease-out;
            }

            header {
                padding: 1rem;
                border-bottom: 1px solid #ccc;
            }

            ::slotted(h1) {
                font-size: 1.25rem;
                margin: 0;
            }

            #main {
                padding: 1rem;
            }

            #actions {
                border-top: 1px solid #ccc;
                padding: 1rem;
                display: flex;
                justify-content: flex-end;
            }

            #actions button {
                margin: 0 0.25rem;
            }
        </style>
        <div id="backdrop"></div>
        <div id="modal">
            <header>
                <slot name="title">Please Confirm Payment</slot>
            </header>
            <section id="main">
                <slot></slot>
            </section>
            <section id="actions">
                <button id="cancel-btn">Cancel</button>
                <button id="confirm-btn">Okay</button>
            </section>
        </div>
    `;
  }
}

//registering custom element named as uc-modal
customElement.define("uc-modal", Modal);

Step 3 Adding logics to our Web component to open the modal

We need to open and close our modal which will be controlled by outside our web component by those who will be calling this web component

  • We need to update our modal.js and add code logic to open and close the modal for this we will be adding method attributeChangedCallback which is web component lifecycle which always run if there is anything changed on our shadow dom

  • We will be adding method observedAttributes which is static method which returns what attribute has changed

  • We will be adding open method which will change the web component attribute that is uc-model and add opened attribute and this method will be called from outside the web component

class Modal extends HTMLElement {
  constructor() {
    super();
    //registering the shadow dom
    this.attachShadow({ mode: "open" });
    this.isOpen = false;

    //template for our modal with slot to take content from outside this web component
    this.shadowRoot.innerHTML = `
        <style>
            #backdrop {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100vh;
                background: rgba(0,0,0,0.75);
                z-index: 10;
                opacity: 0;
                pointer-events: none;
            }

            :host([opened]) #backdrop,
            :host([opened]) #modal {
                opacity: 1;
                pointer-events: all;
            }

            :host([opened]) #modal {
                top: 15vh;
            }

            #modal {
                position: fixed;
                top: 10vh;
                left: 25%;
                width: 50%;
                z-index: 100;
                background: white;
                border-radius: 3px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.26);
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                opacity: 0;
                pointer-events: none;
                transition: all 0.3s ease-out;
            }

            header {
                padding: 1rem;
                border-bottom: 1px solid #ccc;
            }

            ::slotted(h1) {
                font-size: 1.25rem;
                margin: 0;
            }

            #main {
                padding: 1rem;
            }

            #actions {
                border-top: 1px solid #ccc;
                padding: 1rem;
                display: flex;
                justify-content: flex-end;
            }

            #actions button {
                margin: 0 0.25rem;
            }
        </style>
        <div id="backdrop"></div>
        <div id="modal">
            <header>
                <slot name="title">Please Confirm Payment</slot>
            </header>
            <section id="main">
                <slot></slot>
            </section>
            <section id="actions">
                <button id="cancel-btn">Cancel</button>
                <button id="confirm-btn">Okay</button>
            </section>
        </div>
    `;
  }

  //this function will run if there is anything that is changed in the shadow DOM
  attributeChangedCallback(name, oldValue, newValue) {
    if (this.hasAttribute("opened")) {
      this.isOpen = true;
    } else {
      this.isOpen = false;
    }
  }

  static get observedAttributes() {
    return ["opened"];
  }

  open() {
    this.setAttribute("opened", "");
    this.isOpen = true;
  }
}

//registering custom element named as uc-modal
customElement.define("uc-modal", Modal);

Update index.html with the following code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Web Components</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        margin: 2rem;
        font-family: sans-serif;
      }
    </style>
    <script src="modal.js"></script>
  </head>
  <body>
    <uc-modal>
      <h1 slot="title">Please Confirm</h1>
      <p>With your confirmation you agree to pay the full amount!</p>
    </uc-modal>
    <p>Please confirm your choice.</p>
    <button>Show Details & Confirm</button>
    <script>
      const confirmButton = document.querySelector("button");
      const modal = document.querySelector("uc-modal");

      confirmButton.addEventListener("click", () => {
        //  modal.setAttribute('opened', '');
        if (!modal.isOpen) {
          modal.open();
          console.log("Opening...");
        }
      });
    </script>
  </body>
</html>

Step 4: Listening to slot content change and Adding methods to close the modal

  • We will be adding code to listen to slot changes as explained earlier slot is the way to get the content from outside the web component

  • We will be adding hide method to hode the modal

  • We will be adding _cancel method and _confirm method to close the modal on Cancel and Confirm click

Update the modal.js file with the following code

class Modal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.isOpen = false;
    this.shadowRoot.innerHTML = `
        <style>
            #backdrop {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100vh;
                background: rgba(0,0,0,0.75);
                z-index: 10;
                opacity: 0;
                pointer-events: none;
            }

            :host([opened]) #backdrop,
            :host([opened]) #modal {
                opacity: 1;
                pointer-events: all;
            }

            :host([opened]) #modal {
                top: 15vh;
            }

            #modal {
                position: fixed;
                top: 10vh;
                left: 25%;
                width: 50%;
                z-index: 100;
                background: white;
                border-radius: 3px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.26);
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                opacity: 0;
                pointer-events: none;
                transition: all 0.3s ease-out;
            }

            header {
                padding: 1rem;
                border-bottom: 1px solid #ccc;
            }

            ::slotted(h1) {
                font-size: 1.25rem;
                margin: 0;
            }

            #main {
                padding: 1rem;
            }

            #actions {
                border-top: 1px solid #ccc;
                padding: 1rem;
                display: flex;
                justify-content: flex-end;
            }

            #actions button {
                margin: 0 0.25rem;
            }
        </style>
        <div id="backdrop"></div>
        <div id="modal">
            <header>
                <slot name="title">Please Confirm Payment</slot>
            </header>
            <section id="main">
                <slot></slot>
            </section>
            <section id="actions">
                <button id="cancel-btn">Cancel</button>
                <button id="confirm-btn">Okay</button>
            </section>
        </div>
    `;
    const slots = this.shadowRoot.querySelectorAll("slot");
    slots[1].addEventListener("slotchange", (event) => {
      console.dir(slots[1].assignedNodes());
    });
    const backdrop = this.shadowRoot.querySelector("#backdrop");
    const cancelButton = this.shadowRoot.querySelector("#cancel-btn");
    const confirmButton = this.shadowRoot.querySelector("#confirm-btn");
    backdrop.addEventListener("click", this._cancel.bind(this));
    cancelButton.addEventListener("click", this._cancel.bind(this));
    confirmButton.addEventListener("click", this._confirm.bind(this));
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (this.hasAttribute("opened")) {
      this.isOpen = true;
    } else {
      this.isOpen = false;
    }
  }

  static get observedAttributes() {
    return ["opened"];
  }

  open() {
    this.setAttribute("opened", "");
    this.isOpen = true;
  }

  hide() {
    if (this.hasAttribute("opened")) {
      this.removeAttribute("opened");
    }
    this.isOpen = false;
  }

  _cancel(event) {
    this.hide();
  }

  _confirm() {
    this.hide();
  }
}

customElements.define("uc-modal", Modal);

Update the index.html file with the following code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Web Components</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        margin: 2rem;
        font-family: sans-serif;
      }
    </style>
    <script src="modal.js"></script>
  </head>
  <body>
    <uc-modal>
      <h1 slot="title">Please Confirm</h1>
      <p>With your confirmation you agree to pay the full amount!</p>
    </uc-modal>
    <p>Please confirm your choice.</p>
    <button>Show Details & Confirm</button>
    <script>
      const confirmButton = document.querySelector("button");
      const modal = document.querySelector("uc-modal");

      modal.addEventListener("confirm", () => {
        console.log("Confirmed...");
      });

      modal.addEventListener("cancel", () => {
        console.log("Cancelled...");
      });

      confirmButton.addEventListener("click", () => {
        //  modal.setAttribute('opened', '');
        if (!modal.isOpen) {
          modal.open();
          console.log("Opening...");
        }
      });
    </script>
  </body>
</html>