Webcomponents: It's really that easy!
Tags: javascript, webcomponents, tutorial
Published: 2020-01-11 16:29:52

It was 2015 when I first heard about webcomponents, custom elements and the mysterious shadow dom. Browser support was - well - let's call it experimental. 

In a world of polyfills the name polymer seemed fitting for a framework supporting the more or less "Chrome only" technology. But even back then the enthusiasts seemed certain: this is the future. The reasons are obvious. Hooking into how the browser interprets elements provides a fast, snappy user experience that is reusable and contained.

## Where we are

After early adopters experienced constant breaking changes to promising standard suggestions, we are now in a time where webcomponents feel stable, slick and extremely performant. More importantly: it has become simple. 

## The setup

We will not use any third-party libraries in this example, but I suggest taking a look at [lit html](https://lit-html.polymer-project.org/) for basic data binding needs. 


## all-caps

So here is what we want to do: We will create a custom element that will transform its text content to uppercase. Not exactly suspenseful and yes, certainly a little overkill compared to simply using CSS, but it gets the point across nicely. So we begin:

_test.html_
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test custom element</title>
    <script src="all-caps.js" type="module">
</head>
<body>

<all-caps>this is uppercase</all-caps>

</body>
</html>
```

_all-caps.js_

```JavaScript

// 1. create class extending HTMLElement
export class AllCaps extends HTMLElement {}

// 2. Define a new custom element
customElements.define('all-caps', AllCaps)

```

There is a lot to be said regarding these two lines of code. 

First, we are extending HTMLElement. There are some necessities we will need to adhere to, but we'll get to that in the next step.

Next, we define 'all-caps' as a custom element (browser support should not be a problem anymore, but feel free to normalize behavior [gist](https://gist.github.com/sroehrl/e77ddb9a36478c3ca2791cb1d04782a8) if you need to)

## The constructor

So far so good. Now your class need a constructor. This function is executed when the class is initiated. It is important to understand that you will want to account for nesting and continue interpretation. While it is interesting to understand how JavaScript handles this in detail, it is sufficient to simply live by the following rule: **Always start with `super()`**. Don't worry, you will notice that 'this' is not available if you forget. That said, here is how our class looks now:

```JavaScript
export class AllCaps extends HTMLElement {
    constructor() {
        super();
    }
}

```

## Enter the Shadow DOM

The DOM (Document Object Model) is one of those expressions we use without thinking much about it. And one might be interested in looking into the history of HTML and respectively XML, but let's try to foster understanding by example:

In JavaScript, you may have wondered how something like `document.getElementById()` works regardless of context. Needless to say, that is because 'document' accesses (just like your browser) the global DOM tree. Whoever fought with XPath or iframes will have a painful story to tell about handling separated DOMs. On the other hand, separate documents allow for truly encapsulated elements. The Shadow DOM (or sometimes "virtual DOM") is just that. A "sub-DOM" that operates like its own document without the limitations of handling data and state an iframe would have. This is why the Shadow DOM does not inherit styles and provides safe re-usability in all contexts. Sounds great, doesn't it? You can even decide whether the "outside" has access to your element's Shadow DOM or not:

```JavaScript
export class AllCaps extends HTMLElement {
    constructor() {
        super();
        // attach a shadow allowing for accessibility from outside
        this.attachShadow({mode: 'open'});
    }
}
```

At this point running _test.html_ will show you a blank page as we work with a "new" DOM. However, this does not mean we have lost our content. Although I would prefer working with nodes, let's wrap up our code to get the first version of our intended output:

```JavaScript
export class AllCaps extends HTMLElement {
    constructor() {
        super();
        // attach a shadow allowing for accessibility from outside
        this.attachShadow({mode: 'open'});
        
        // write our uppercased text to the Shadow DOM
        let toUpper = this.firstChild.nodeValue.toUpperCase();
        this.shadowRoot.innerHTML = toUpper;
    }
}

```

We got it! This is functional and refreshing _test.html_ should show the expected outcome.

## Advanced

Let's play around with some additional basics. 

### Applying style

_NOTE:_ I would normaly structure this a little different, but to contain the bits we are talking about, let's do the following:

After the constructor, we add another function called "attachTemplate"

```JavaScript
attachTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
        <style>
        :host{
         color: red;
        }
        </style>`;
    this.shadowRoot.innerHTML += template.innerHTML;
}

```

You might wonder about ":host". This selector refers to the element itself. In order to execute this function, we want to call it in our constructor:

`this.attachTemplate()`

Note that you could also make use of e.g. 'connectedCallback' as a function name but I want to keep this tutorial contained to the basics.
Our class should now look like this:

```JavaScript
export class AllCaps extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: 'open'});
        let toUpper = this.firstChild.nodeValue.toUpperCase();
        this.shadowRoot.innerHTML = toUpper;
        this.attachTemplate();
    }
    attachTemplate() {
        const template = document.createElement('template');
        template.innerHTML = `
        <style>
        :host{
         color: red;
        }
        </style>`;
        this.shadowRoot.innerHTML += template.innerHTML;
    }
}
```

Reloading _test.html_ should now give you not only uppercase, but also a red color (please consider single responsibility in real scenarios).

### Slots

Another (here dirty) introduction at this point could be the use of slots. Slots can be named or referring to the complete content of the element. Let's try it out to get the hang of it:

In the literal string for our files, add the tag `<slot></slot>`, resulting in the following attachTemplate function

```JavaScript
attachTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
        <slot></slot>
        <style>
        :host{
         color: red;
        }
        </style>`;
    this.shadowRoot.innerHTML += template.innerHTML;
}
```

Refreshing your browser, you will notice that the original content of our tag has been added to our DOM.

  

### Attributes & data

As a last introduction, let's look into attributes. Again, this will be a non-sensical example, but I think it explains the concept well. 
In our _test.html_, we will give our tag the attribute "addition" with the value "!"

`<all-caps addition="!">hi there</all-caps>`

Next, we will edit our template sting again and add `${this.addition}` after our slot. 

```JavaScript
attachTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
        <slot></slot>
        ${this.addition}
        <style>
        :host{
         color: red;
        }
        </style>`;
    this.shadowRoot.innerHTML += template.innerHTML;
}
```

We now need to handle the attribute and at least account for it not being set. To do so, we should probably create a new function, but I will once again quickly "hack" it. In the contructior function, **prior** to executing "attachTemplate", we can add

```JavaScript
if(this.hasAttribute('addition')){
    this.addition = this.getAttribute('addition')
} else {
    this.addition = '';
}
```

Our class now looks like this:

```JavaScript
export class AllCaps extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: 'open'});
        let toUpper = this.firstChild.nodeValue.toUpperCase();
        this.shadowRoot.innerHTML = toUpper;
        if(this.hasAttribute('addition')){
            this.addition = this.getAttribute('addition')
        } else {
            this.addition = '';
        }
        this.attachTemplate();
    }
    attachTemplate() {
        const template = document.createElement('template');
        template.innerHTML = `
        <slot></slot>
        ${this.addition}
        <style>
        :host{
         color: red;
        }
        </style>`;
        this.shadowRoot.innerHTML += template.innerHTML;
    }

}
```

Refresh your browser to see the result. 

## Conclusion

This tutorial is meant to help you understand basic handling of custom elements and the Shadow DOM. As stated in the beginning, you probably want to use a library like lit-html to simplify things and you most certainly want to work a little cleaner (Fought with myself a lot between leading by example and keeping the code as consice as possible). However, I hope this gives you a good start and helps setting of the spark to dive deeper.

Today we can assume that webcomponents will be dominating the web and slowly push out performance intensive frameworks like Angular. Whether you are at the beginning of your career or a battle-tested React enthusiast, it does make sense to familiarize yourself with the direction the web is moving to. Enjoy!