A Quick Guide to Custom HTML Elements
Tags: html, javascript, webcomponents, tutorial
Published: 2021-10-24 05:08:09

After getting to know the component based system of modern frameworks such as React and Angular, i was immediately interested when i came across the term "Web Components". After taking a deep dive into the subject, i thought it would be worth sharing. So, today im going to provide a quick guide to Web Components, how they operate, and how we can create our own custom HTML elements by applying this concept!

## What are Web Components?
   
A Web Component serves as a label for any HTML Element that possesses its own underlying DOM tree, CSS styles, and even scripts, that are separate from the reset of the DOM and encapsulated completely by that element. This underlying DOM tree is refered to as the `Shadow DOM`.

### The Shadow DOM

To further understand the `Shadow DOM`, take the contents of the following HTML `<input>` as an example:

![shadow DOM of an input element of type "range"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t67wxs0xcf6qvbyfqwo9.png)


Inside of the `input` the first thing we see is `#shadow-root`. This simply represents the root of the `<input>` element's shadow DOM, much like how `<html>` is the root element of the document. The element containing the `#shadow-root` itself is refered to as the "shadow host", which in this example is the `<input>`. Everything that comes after `#shadow-root` is the contents of the element's shadow DOM. Simple, right? :) 

Not all, but many elements are capable of having a `Shadow DOM` attached to them, which can serve useful if you require a quick way to provide encapsulation and abstraction in your code.

In this particular case, the `Shadow DOM` of the input element contains only two `<div>` elements. As you may have been able to tell by the ids of each element, these are used to create the track, and thumb peices of the range slider.

This is a _perfect_ example of the `Shadow DOM` in action. It allows things like `<video>` and `<audio>` elements to hide their moving parts and functionality away from the rest of the document, simplifying and keeping the program organized. 

We can take this a step further, by implementing the Shadow DOM API to create our own custom HTML components..😁

## Creating our first Custom HTML Element

Before starting to build our custom element, we first need to understand some criteria it has to meet to be considered one: 

1.  It has to have a constructor (usually via a `class`)
2. It needs to posses a `Shadow DOM`
3. It must be registered within the `CustomElementRegistry`
4. It can __optionally__ use a `<template>`

If any of these steps dont immediately make sence, dont sweat it, everything will become clear as i walk you through it.

For demonstration in this short tutorial ill be making a custom HTML element named `<type-writer>`. The final product will look like this:

![my custom "typewriter element"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c3ierny0lt51eh1q73ys.png)

I decided to make a simple custom element which consists of a `<textarea>`, and a some `<button>` elements to serve as keys to show text onto the screen.

An easy example that should clearly demonstrate how we can create somewhat complex custom elements using this API.

Without further ado, lets start with the first step..👇

## Creating a Web Component Constructor

Before we do anything else, its mandatory that we create a constructor function for our custom element. Its purpose is to initiate our component and attach any functionality it might have to it, and will be called each time a new instance of our web component is created.

Below i create the constructor for our `<typewriter>` element using a class:
 
```javascript
// Extend generic HTMLElement interface
class Typewriter extends HTMLElement {
 constructor() {
  super();
  // implement functionality...
 }
}
```

I've named the class `Typewriter`, though this dosent serve as the name we'll use to write it into our HTML (`<type-writer>`), so you can call it whatever you want. Ill show how you can define a tagname for it in a later section.

When creating your constructor, its required that you extend the functionality of an existing built-in HTML element, or the generic `HTMLElement` interface. This is so that your custom element inherits all of the same required properties that all built-in elements do. Otherwise your custom element wouldn't be compatible with the DOM.

There are two types of web components that can be created, based on the interface that you're extending:

* __Customizable components__: Custom elements where the constructor of which, extends upon the functionality and properties of an already existing built in element. 

Its constructor would be similar to -
```javascript
// Extend functionality of a <div> element
class Superdiv extends HTMLDivElement {
constructor() {
 super();
}
}
```

And would be shown in HTML as -
```html
<div is="super-div"></div>
```

* __Autonomous components__: Custom elements where the constructor of which, extends the functionality of the generic `HTMLElement` interface. These elements apply their own functionality, and share no properties in common with other built-in HTML elements other than those defined in the `HTMLElement` interface (which serves as the bare-bones, or, minimum required properties for every HTML element). 

Its constructor would be similar to -
```javascript
// Extend the generic HTMLElement interface
class MyElement extends HTMLElement {
 constructor() {
  super();
 }
}
```

And would be shown in HTML as -
```html
<my-element></my-element>
```

In this case, our `<type-writer>` element is an __autonomous__ component, because it extends the `HTMLElement` interface. I decided on an autonomous component because i didn't find it nesessary to extend another elements functionality, but mainly because i find writing `<type-writer>` into HTML rather than somthing like `<div is="type-writer"></div>` much more attractive..👌😎

## Attaching a Shadow DOM

Now that we've got a container for our elements functionality, we need to attach a Shadow DOM onto our element upon its initiation.

```javascript
// Extend generic HTMLElement interface
class Typewriter extends HTMLElement {
 constructor() {
  super();
  // attach shadow DOM to element
   let shadow = this.attachShadow({mode: "closed"});
  // implement functionality...
 }
}
```

After calling `super`, i call the `attachShadow` method of our new element (which was inherited from `HTMLElement`) which returns the newly created `#shadow-root`, which i store in the variable `shadow`. 

The one parameter it takes in is an object which contains a couple of configuration options. The `mode` property indicates weather or not the elements within the `#shadow-root` of our  element are accessable outside of the shadow host. Ive set it to `"closed"` so that they aren't accessable, but you can use `"open"` as well depending on your programs reqirements.

We now have a reference to our shadow root, so we can go ahead and start appending content to it to construct our element!

## Filling Our Shadow DOM

I would say theres two good ways to go about adding content to the `Shadow DOM` once its attached; you can create elements and append them to the `#shadow-root` via normal DOM methods, or you could utilize a `<template>`.

### The `<template>` Element

The HTML template element is a unique element, used to hold content which will later be implemented.

```html
<!-- a simple template example -->
<body>
 <template id="my-template">
  <!-- template content -->
  <p>A simple template!</p>
 </template>
</body>
```

`<template>` elements are parsed in HTML, but not rendered. Each `<template>` will have its own `content` property, which is `DocumentFragment`(much like a React fragment) of its contents. We can then clone this content and append it to our elements `#shadow-root`.

The `<template>` element can also be used in conjunction with the `<slot>` element, which serves as a placeholder for you to add dynamic content into a template. Its a tad out of the scope of this guide, but you can read more about it [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot).

Below i create a template containing the content that will be inside of the `#shadow-root` of my `<type-writer>` element, and append it:

```html
  <template id="typewriter-template">
    <style>
    /* applying default styles to our element */
      textarea {
        background: black;
        color: limegreen;
        width: 200px;
        height: 70px;
        box-sizing: border-box;
        border: none;
        padding: 0.5em;
      }

      div {
        width: 200px;
        display: grid;
        height: 200px;
        grid-template-columns: repeat(4, auto);
      }

      span {
        height: 270px;
        width: 200px;
        display: grid;
        border-radius: 10px;
        overflow: hidden;
      }
    </style>
    <span> 
    <!-- screen -->
      <textarea readonly placeholder="..."></textarea>
    <!-- button container -->
      <div></div>
    </span>
  </template>
```

```javascript
class Typewriter extends HTMLElement {
 constructor() {
  super();
  // attach shadow DOM to element
   let shadow = this.attachShadow({mode: "closed"});
  // Apply template
   let template = document.getElementById("typewriter-template");
shadow.appendChild(template.content.cloneNode(true));
  // implement functionality...
 }
}
```

Lastly, before implementing the final step, i'll add in all of the functionality for my custom `<type-writer>` element, completing our constructor:

```javascript
class Typewriter extends HTMLElement {
  constructor() {
    super();

// attach shadow DOM
    let shadow = this.attachShadow({ mode: "closed" }),
      template = document.getElementById("typewriter-template");

// implement template
shadow.appendChild(template.content.cloneNode(true));

// Adding keys and additional functions
    let keys = shadow.querySelector("div");
    let screen = shadow.querySelector("textarea");
    let typed = new Event("typed");
    screen.addEventListener("typed", () => {
      screen.innerHTML = screen.innerHTML + "|";
    });
    for (let i = 97; i <= 122; i++) {
      let key = document.createElement("button");
      key.addEventListener("click", (e) => {
        backspc();
        screen.innerHTML = screen.innerHTML + e.target.innerText;
        screen.dispatchEvent(typed);
      });
      key.innerText = String.fromCharCode(i);
      keys.appendChild(key);
    }
    let del = document.createElement("button"),
      spc = document.createElement("button");
    del.innerText = "DEL";
    function backspc() {
      let l = screen.innerHTML.split("");
      l.pop();
      console.log(l);
      screen.innerHTML = l.join("");
    }
    del.addEventListener("click", () => {
      backspc();
      backspc();
      screen.dispatchEvent(typed);
    });
    keys.appendChild(del);

    spc.innerText = "SPC";
    spc.addEventListener("click", () => {
      backspc();
      screen.innerHTML = screen.innerHTML + " ";
      screen.dispatchEvent(typed);
    });
    keys.appendChild(spc);
  }
}
``` 

## Registering our `<type-writer>` Element

Before we can use our new `<type-writer>` tag in our HTML code, we lastly need to register our component within the `CustomElementRegistry`. The `CustomElementRegistry` interface is implemented by the `customElements` object, which is where constructors for custom elements are stored, and can be accessed.

We can register our new element by using the `customElements.define()` method:

```javascript
customElements.define("type-writer",Typewriter);
```

The first parameter is the tagname we want for our new element. This can be anything as long as a dash (-) is included within it. And then our second parameter is simply the constructor thats associated with our new custom element.

Once thats done, you can use it in your HTML and refer to it in your CSS and Javascript just as you would with any other built-in element! Pretty cool, huh?

```html
<type-writer></type-writer>
```
## Conclusion

And there you have it! I hoped this guide served useful for understanding how Web Components work, and how we can use the Shadow DOM and Custom elements APIs to create our own HTML elements.

Good luck, and happy coding! 😁