Javascript

Learn about what web components are and how to use them.

In 2013, Facebook introduced component-based architecture by announcing React. Soon after that, most other frameworks, such as Angular, adopted it. Component-based architecture allows developers to break down their applications into smaller sections, which are called components. Each of these sections often has a single responsibility and may have one or more child components. Just like LEGO bricks, you can orchestrate these components and build a scalable, maintainable and reliable solution.

While building an application, we often realize that many of our UI components look and behave the same. These components can be written once (as reusable components) and be used in other parts of the application.

This is great, but can still be improved. Imagine:

  • We could create a component with Angular and use it in an application that is written in Vue
  • Our component could have the same look and behavior, no matter what is changed in the global style
  • We could have a style encapsulation and still be able to customize our component

This is where web components shine. Web components provide us with all of these missing bits and pieces.

What are Web Components?

Web components are basically platform agnostic components that are written in a way that they have the actual style encapsulations. Almost all modern JavaScript frameworks support web components. You can check this website by Rob Dodson to find out if the framework you are using today supports web components or not.

Web components consist of three main technologies:

  • HTML template
  • Custom elements
  • Shadow DOM

Previously, HTML imports were also a part of it, but with ES6 modules, HTML imports became deprecated. The Polymer team is working on an HTML Module to provide the ability to load an HTML template into JavaScript code.

So let’s go through each of these three technologies together.

HTML Template

You may already be familiar with the template concept. It is mostly useful once you have a part of your HTML that needs to be repeated multiple times. Imagine you are going to make a contact list and you need to create a contact card for each of your contacts. In this case, it totally makes sense to create a template for your contact card and reuse it.

Template tags give you the possibility of creating a block of HTML and, with the help of JavaScript, add the content to it, clone it and then add it to the DOM.

The benefit of using HTML tags is that browsers are going to parse them but not render them. You need to manually clone the template and attach it to the DOM. In this way, you boost the performance, because no matter how many times you clone the template, add the content to it, and attach it to the DOM, browsers are going to parse the template once.

Let’s look at the following example:

Here is our HTML template:

<template id="contact-card-template">

<p id="name"></p>

<p id="email"></p>

</template>

Here is our JavaScript:

// Get reference to the template
const template = document.querySelector('#contact-card-template');

// Add the first name
const info = template.content.querySelector("#name");
info.textContent = "Sherry";

// Add the first email
const info = template.content.querySelector("#email");
info.textContent = "sherry@berry.com";

// Clone the new card and add it to the DOM
const imageGallery = document.querySelector("#contacts");
const clone = document.importNode(template.content, true);
imageGallery.appendChild(clone);

You can find the code on StackBlitz.

Custom Elements

With custom elements, developers gain the ability to create their own DOM elements. In other words, they can define new HTML elements. These elements can either be totally new elements, which means that they extend the HTMLElement interface, or extend one of the existing HTML elements.

It’s also possible to inherit from one of the existing elements. But normally that’s not the recommended way to create an element. If you decide to inherit from a native element, you should keep in mind that your element should support all the native APIs of the element you are inheriting from. This can be quite complicated, because just imagine how many types an <input> tag can support.

Create New HTML Elements

In order to create a totally new HTML element all you need to do is to create a class which extends HTMLElement and then define your own custom tag.

See the example below:

class FancyInputElement extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('fancyInput');
    this.innerHTML = template.innerHTML;
  }
}

customElements.define("fancy-input", FancyInputElement);

You can find the code on Stackblitz.

Extend an Existing HTML Element

In order to extend or upgrade a native element, you need to create a class which extends the native HTML element that you need, then define your own custom element. The difference from the previous case is that you need to add a third parameter to customElements.define and specify which native element you are extending. In order to tell the browser that you want to use this new behavior you need to use the ‘is’ attribute in your HTML.

Here is a simple example of extending <a> tag.

Inside connectedCallback():

class FancyLinkElement extends HTMLAnchorElement {
  connectedCallback() {
    this.addEventListener("mouseover", e => {
      this.setAttribute("style", "color:pink; font-style: bold;");
    });

    this.addEventListener("mouseout", e => {
      this.setAttribute("style", "color:black; font-style: bold;");
    });

    this.addEventListener("click", e => {
      this.target="_blank";
    });
    
  }
}

customElements.define("fancy-link", FancyLinkElement,  { extends: "a" });

<a href="https://www.webcomponents.org/" is="fancy-link">Follow me to the other side!</a>

 

You can find the code on Stackblitz.

Custom Element Lifecycle

Because custom elements are extended, the HTMLElement interface comes with the following methods:

  • connectedCallback(): This method is automatically called as soon as the component is attached to the DOM. This is the best place to get hold of an element’s input attribute and define the rendering behavior
  • attributeChangedCallback(): This method is automatically called as soon as any of the input attributes of your element are changed
  • disconnectedCallback(): This method is automatically called when the custom element is disconnected from the document's DOM
  • adoptedCallback(): This method is automatically called when the custom element is moved to a new document

Shadow DOM

Shadow DOM allows you to encapsulate your component, so you make sure the component looks and behaves the same, no matter what happens in the global CSS. This means there won't be any style leak in or out of your component. 

How Do I Create a Web Component?

So let’s use all these three technologies together and create a contact card web component.

Inside the template, we defined placeholders for ‘name’ and ‘email’. 

<template id="contact-card-template">

<p id="name"></p>

<p id="email"></p>

</template>



<fancy-contact

name="Sherry List"

email="sherry@berry.com">

loading...

</fancy-contact>

Inside constructor(), we should specify that we have a ShadowRoot and the cloned template needs to be appended to the Shadow DOM instead of directly to the DOM.

  constructor() {
    super();
    const template = document.querySelector('#contact-card-template');

    // Shadow DOM
    this.attachShadow({ "mode": "open" });
    this.shadowRoot.appendChild(template.content.cloneNode(true));

Later inside connectedCallback() we need to get the reference from the template and add the attribute that is passed to our elements. Therefore, we created a _render() function and called it.

  connectedCallback() {
    this._$name = this.shadowRoot.querySelector("#name");
    this._$email = this.shadowRoot.querySelector("#email");
    this._$avatar = this.shadowRoot.querySelector("#avatar");
    this._render(this);
  }


  _render({ name, email, avatar }) {
    this._$name.textContent = name || 'N/A';
    this._$email.textContent = email || 'N/A';
  }

The last missing part is to react to any attribute input changes. That means that if, all of a sudden, the name attribute changed from ‘Sherry’ to ‘Sherry List’, we should be able to react and update the DOM. Therefore, we need to use attributeChangedCallback().

  static get observedAttributes() {
    return ["name", "email", "avatar"];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this[name] = newValue;
  }

You can find the code on Stackblitz.

Best Practices

While creating web components you should remember to follow certain guidelines to make sure that you are creating a reusable, extendable and user-friendly web component with a consistent API.

Here is a list of available guidelines and best practices:

This article is written based on the talk I gave with Ana Cidre at We are developers conference.


Are you interested in learning more about JavaScript frameworks with web components? Check out our recent analysis of the State of JavaScript 2018 Survey for React, Angular and Vue.


Sherry List
About the Author

Sherry List

For the past 15 years, Sherry has worked with a variety of web technologies and is currently focused on Angular. She lives in beautiful Copenhagen, where she works as a front-end lead developer at Nordea Bank. Apart from her everyday job, she is a co-organizer of the ngVikings conference, as well as ngCopenhagen, GDG Copenhagen and Women Techmakers Copenhagen. She loves animals and supports various non-profit animal protection organizations. Feel free to connect with her on Twitter

Related Posts

Comments

Comments are disabled in preview mode.