From Web Components in Action by Ben Farrell

This article walks you through the creation of your first web component.


Take 37% off Web Components in Action. Just enter code fccfarrell into the promotional discount code box at checkout at manning.com.


Intro to HTMLElement

Prior to learning the basics of Web Components, I didn’t know what an HTMLElement was. You might not either—it’s an easy thing to never come across, because although it’s a core concept in how the DOM works, we’ve typically never worked with it directly prior to Web Components.

The reason for this is that when you add an element to your page, it works. You don’t necessarily need to know how an <input> tag is related to a <button> or how a <div> is related to an <img>.

To explain, we’ll have to get a bit into the concept of inheritance. It’s a popular concept in Object Oriented Programming, so let me start with an example.

Pretend you’re at a zoo. While you’re there, you notice that all the animals have specific things in common. Animals need to eat, breathe, sleep, and move around. Some animals are different than others. Mammals have fur, have babies instead of laying eggs, and are warm-blooded. Mammals have all the base characteristics of animals, but there are extra rules when you call something a mammal. You could even go further and consider mammals like Tigers, Lions, and Panthers as a type of cat. Cats also have some specific things in common like whiskers, claws, and eating meat.

In object-oriented programming, we can say that a cat inherits from a mammal, and a mammal inherits from an animal. If you were writing code, you might start by defining an “Animal” object (or Class to be more specific). Your “Animal” might have functions that you can call to make it breath(), sleep(), and eat().

Next you might want to create a “Mammal” object. It’d be tiresome and repetitive to write code for breath(), sleep(), and eat() again for the “Mammal” object. Because this is similar to “Animal”, we can use inheritance, and when creating that “Mammal” object we say “Mammal” extends “Animal”. “Mammal” automatically gets all the functionality as “Animal”, but we can add more specific functionality like growFur(). We can even create a “Cat” object that inherits from “Mammal”, and because “Mammal” inherits from “Animal”, “Cat” have all the functionality of “Mammal” and “Animal”.


Figure 1. A non-scientific example of inheritance in the animal kingdom


Our zoo example is a lot like the HTMLElement. With a few exceptions like SVG, any element you put in your HTML is inherited from HTMLElement. Although HTMLElement isn’t the bottom rung of the inheritance chain as far as the browser is concerned (as we can keep going with “Animal” to multi-cellular organism, to living things, etc), it serves as our starting point for Web Components.

 

To give real examples of inheritance on actual elements, <span>, <div>, and <button> inherit from HTMLSpanElement, HTMLDivElement, and HTMLButtonElement respectively. In fact, you can see for yourself. Open up the browser console and type the following:

 
 document.createElement('div').constructor
 

The console returns:

 
ƒ HTMLDivElement() { [native code] }
 

What we’re doing here is creating a new <div> element and asking it what it’s using for a constructor. The constructor is essentially the blueprint for an object that is created. In this case, the blueprint is HTMLDivElement.

Feel free to play around with your favorite elements. Button is another we can try:

 
 document.createElement('button').constructor
 

Which gives us:

 
 ƒ HTMLButtonElement() { [native code] }
 

Figure 2. Although there are a large number of classes that inherit from HTMLElement, here are three that produce common DOM elements which we use all the time, with the tags we write in our HTML


Rules for Naming your Element

One interesting thing about HTML is that you can make up any name for a tag, drop it on you page and it acts like a <div>.

Try it in your page:

 
 <randomElement>Hi!</randomElement>
 

You’ll see the text “Hi!”, as if you were using a <div>. Now, the question is: what are we inheriting from here? Let’s try it in our console:

 
 document.createElement('randomElement').constructor;
 

We get back:

 
 ƒ HTMLUnknownElement() { [native code] }
 

Were you expecting HTMLUnknownElement? Probably not. We created an invalid element. Because it’s invalid, it inherits from a special “Unknown” class and we can’t extend its functionality.

Why is it invalid? It’s not because we can’t invent our own element names when we create our own components. It’s because there’s a simple naming convention to follow. This naming convention is a hard requirement for the custom element specification, which is to use a dash (“-“) in your name. Under the hood, it allows the browser to differentiate between custom elements and native elements.  It makes sense when you think about it. Not only do readers like you create their own custom components, but browsers will likely come out with new elements as well. A common use of web components will likely be tiny pieces of common UI. If something useful like a progress bar was created by you, other Web Component developers, and made it into the browsers as a native feature, you might imagine how much of a mess it’d be if everyone created something named <progressbar>.

So, adding a dash (“-”) in your element name is a standard and required practice here. If your desired element name is <progressbar>, try again with a dash: <progress-bar>. Ideally, you’d want to give it a “namespace”.  A namespace is used to indicate a group that your component belongs to. For example, in Google’s framework for building Web Components, called Polymer, any UI component built with their design system “Material” has a namespace of “paper”. If you go to their Web Component Github repo, you can find “paper-tooltip”, “paper-dropdown-menu”, and “paper-toggle-button”. Note that some of these have two dashes, and this is perfectly okay; you need one or more to be valid. The important takeaway here is that Google defines a namespace to indicate a set of related components, and then names the specific component after the dash. You certainly aren’t required to follow the same logic, you just need a dash.


Figure 3. A small sampling of Google’s Paper Elements. Note that these related UI Web Components have the prefix “paper”. Google also uses the prefix “iron” for core elements, and “neon” for animation related elements.


Let’s revisit our “randomElement”, but name it with a dash this time to follow proper conventions:

 
 document.createElement('random-element').constructor;
 

Good news, this prints the following in our console:

 
 ƒ HTMLElement() { [native code] }
 

Defining your Custom Element (and Handling Collisions)

It’s one thing to invent a name for a tag and create it versus giving the tag definition before creating it. It’d be fairly useless to create your own tag without giving it some custom behavior. We’ll need to go beyond HTMLElement and override it with our own logic.

Thankfully, it’s easy to do that. This brings us to, in my opinion, the biggest and most useful piece of the Web Components API. With one simple line of Javascript, and using an empty class that extends HTMLElement, we can take our desired element name and give it meaning:

 
 customElements.define('my-custom-tag', class extends HTMLElement {});
 

One aspect can cause you problems – but it’s one that won’t affect you until you get into more complex things. All the same, it’s a good time to bring it up now because customElements.define throws an error if you’ve already defined a tag!

For now, we can mimic this bad behavior by calling customElements.define twice in a row:

 
 customElements.define('my-custom-tag', class extends HTMLElement {});
 customElements.define('my-custom-tag', class extends HTMLElement {});
 

The error we get is the following:

 
 Failed to execute 'define' on 'CustomElementRegistry': this name has already been used with this registry
 

Thankfully, this is easy enough to handle. We can determine if our custom element is already defined by asking if customElements.get(‘my-custom-tag’) returns something. By wrapping this in an if/then statement, we ensure that our element is only defined when we first call it

 
 if (!customElements.get('my-custom-tag')) {
    customElements.define('my-custom-tag', class extends HTMLElement {});
 }
 

Now, extending HTMLElement to define a custom element is super powerful, but don’t go too crazy yet. You might think that extending HTMLDivElement or HTMLButtonElement works too, but unfortunately, not yet. Although the customElement specification says this is okay, browsers haven’t yet implemented this functionality. HTMLElement is the only native element definition we’re allowed to extend and create custom elements from right now—anything else appears to work, but when you use your element you’ll get an error:

 
 Uncaught TypeError: Illegal constructor: autonomous custom elements must extend HTMLElement
 

Note the “must extend” part of the error as well. Even passing HTMLElement without extending it into customElements.define results in this behavior when you use your new element.

Extending HTMLElement to Create Custom Component Logic

The easiest way to write your custom component, as you’ve seen, is to use a newer Javascript feature called a “Class”. Purists argue that Javascript didn’t give us a “real” class feature. Indeed, in other languages, proper Classes aren’t runnable code; they’re the blueprints to create instances by following the instructions of that blueprint. In Javascript, everything is runnable code, and classes are clones of objects created when they’re instantiated. In proper terminology, Javascript is known as a “prototype-based language”.


Figure 4. Whereas traditional languages offer classes as a “blueprint”, Javascript is a prototype language, and classes are a nice syntax for cloning objects from other objects


Regardless, Javascript Classes provide a great and readable way to express how our custom element works and also how it inherits from an HTMLElement. The readability aspect is a primary reason for using them, such as how it makes Javascript look more like other “Object Oriented” languages.

Let’s start with an empty class, which inherits from HTMLElement.

 
 class MyCustomTag extends HTMLELement {}
 

Now, with your new element definition, you can create something custom:

 
 customElements.define('my-custom-tag', MyCustomTag);
 

Figure 5. Our HTMLElement inheritance diagram modified to include your own custom elements at the same level as the native ones


No custom logic exists because our element is based off an empty class. For now, it acts like HTMLElement does, but we can fix that by using the connectedCallback method in this class.

Let’s modify our class to give us some indication that we’ve an effect on it. Here’s a snippet we can include on our page to define our element.

Listing 1 Giving our custom tag some custom logic

 
 <script>
      class MyCustomTag extends HTMLElement {
           connectedCallback() {
                alert(‘hi from MyCustomTag’);
           }
      }
     
      if (!customElements.get('my-custom-tag')) {
         customElements.define('my-custom-tag', MyCustomTag);
      }
 </script>
  

To see this in action, drop your custom tag in the body of your HTML

 
 <body>
      <my-custom-tag></my-custom-tag>
 </body>
 

When you try this out, you won’t see anything visible on your page except for the alert dialog that pops up. Now that we’ve verified that we can inject logic into our new Web Component, let’s build up our Web Component to be more visible. To do this, I need to mention scope and how it can work for us in our Web Component. Often times, the keyword this can be a bit confusing. This is due to the fact that it’s not always clear what scope this is referring to. For example, you might call a function from a timer event. When the function is called, referring to this is from the scope of your timer and not the scope of the code that originally called it.

Luckily, with Web Components and classes, we can use this in some dead simple and easy to read ways! With a few notable exceptions, this in your component refers to the element itself. This includes custom methods and properties you introduce on the element, but also any methods or properties that the element already has. To put it another way, any method or property you might use from an ordinary non-custom element can be used in this scope and referenced by this. Keyword this is the scope of our new custom element.

Examples for what you can call from this inside your custom element class includes everything inherited from HTMLElement, like getting the element’s CSS with this.style, getting the element’s height with this.offsetHeight, or adding an event listener when the user clicks on your component with this.addEventListener(‘click’, callback).

To give our element some content, let’s start with the innerHTML property. Again, innerHTML can be used on any element and serves to set the HTML content inside the element. We can use it similarly here.

 
 this.innerHTML = ‘<h2>My custom element</h2><button>click me</button>’;
 

Although it’s not incredibly readable to throw HTML in a string like this in one line, for our purposes, to demo something small, let’s run with it.

Let’s also add a little color to the background of our component, as well as padding, text color, and the display style.

 
 this.style.backgroundColor = ‘blue’;
 this.style.padding = ‘20px’;
 this.style.display =  ‘inline-block’;
 this.style.color = ‘white’;
 

Putting it all together, we have:

Listing 2 A Complete, but simple, Web Component example

 
 <script>
      class MyCustomTag extends HTMLElement {
           connectedCallback() {
                  this.innerHTML = ‘<h2>My custom element</h2><button>click me</button>’;
                  this.style.backgroundColor = ‘blue’;
                  this.style.padding = ‘20px’;
                  this.style.display =  ‘inline-block’;
                  this.style.color = ‘white’;
          }
      }
  
      if (!customElements.get('my-custom-tag')) {
         customElements.define('my-custom-tag', MyCustomTag);
      }
 </script>
  
 <body>
     <my-custom-tag></my-custom-tag>
 </body>
 

Congratulations! You’ve taken your first step on the road to creating your own Web Components.


If you’re hungry for more, read the first chapter of the book here and see this slide deck.