Anatomy of a Web Component: the Low-level Basics
Tags: webcomponents, javascript, html
Published: 2023-11-08 11:46:53

## Introduction

I've been learning about web components from [Rob Eisenberg's Web Component Engineering course](https://www.bluespire.com/p/web-component-engineering), and thought I'd crystallise some of my learning by writing about it. Here, therefore, is a very basic web component that demonstrates some fundamental characteristics of web components that we'll build on in the future (I'm only a few lessons in, and there's a LOT to go).

What we're going to do here is to create a `my-counter` element, which is really very simple. All it does is render the value contained in the `count` property into the DOM. So this:

```html
<my-counter count="3"></my-counter>
```

will render this:

```
3
```

If you don't provide a `count` it will output `0`.

Here's the full code to start with, then we'll dive into each line and dissect.

First, the HTML:

```html
<template id="my-counter">
    <div id="count"></div>
</template>

<my-counter count="0"></my-counter>
```

And now the JS:
```js
class MyCounter extends HTMLElement {
  static observedAttributes = ["count"];
  static #fragment = null;

  #view = null;
  #count;

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    if (this.#view === null) {
      this.#view = this.#createView();
      this.shadowRoot.appendChild(this.#view);
      this.#count = this.shadowRoot.getElementById("count");
    }

    this.countChanged();
  }

  attributeChangedCallback() {
    this.countChanged();
  }

  countChanged() {
    if (this.#count) {
      const value = this.getAttribute("count") ?? 0;
      this.#count.innerText = value;
    }
  }

  #createView() {
    if (MyCounter.#fragment === null) {
      const template = document.getElementById("my-counter");
      MyCounter.#fragment = document.adoptNode(template.content);
    }
  
    return MyCounter.#fragment.cloneNode(true);
  }
}

customElements.define("my-counter", MyCounter);
```

## Declaring the `MyCounter` class.

```js
class MyCounter extends HTMLElement {
```

`MyCounter` extends the base `HTMLElement`, making it a new kind of HTML element. We can now use this `MyCounter` class to add our own behaviours whilst keeping all the properties and methods of a normal `HTMLElement`.

### Observed Attributes

```js
  static observedAttributes = ["count"];
```

The `static observedAttributes` property defines a list of attributes that the component will monitor. When the `count` attribute changes, `MyCounter` responds by updating the component's displayed value.

### Efficient Template Handling

```js
  static #fragment = null;
```

By declaring a `static #fragment` property and storing the template fragment there when the first instance of the `MyCounter` HTMLElement is created, `MyCounter` avoids redundant template adoptions for multiple instances. This shared fragment serves as the blueprint for each instance's view.

### Instance-specific DOM

```js
  #view = null;
```

Once the template has been adopted, we can `clone` it for each instance of the counter. The `#view` property holds the cloned template for the component instance. Unlike the shared fragment, this property is instance-specific, creating a unique view for each counter on the page.

### Private Element References

```js
  #count;
```

`MyCounter` uses a private `#count` field to hold a reference to the specific DOM element displaying the count. This reference is set in `connectedCallback`, ensuring that each component instance updates its own display independently.

## Lifecycle Callbacks

### Component Connection

```js
  connectedCallback() {
    // If the view has not yet been initialised, we need to
    // create it by adopting the template into the DOM, then cloning.
    if (this.#view === null) {
      this.#view = this.#createView();
      this.shadowRoot.appendChild(this.#view);
      this.#count = this.shadowRoot.getElementById("count");
    }

    this.countChanged();
  }
```

When an instance is added to the DOM, `connectedCallback` is triggered. This callback is responsible for initialising the instance's view if it hasn't been already, appending it to the shadow DOM, and caching the `count` element.

The `countChanged` method also needs to be called here, because the `attributeChangedCallback` may not have fired yet (i.e. the `count` property might not have a value) and and so we need to display the default value of 0.

### Responding to Attribute Changes

```js
  attributeChangedCallback() {
    this.countChanged();
  }

  countChanged() {
    // If the id is present it means the element has been connected to the DOM.
    if (this.#count) {
      const value = this.getAttribute("count") ?? 0;
      this.#count.innerText = value;
    }
  }
```

The `attributeChangedCallback` and `countChanged` methods work hand in hand to update the component's display whenever the `count` attribute changes. This ensures the UI is always in sync with the component's state.

### Template Adoption and Cloning

```js
  #createView() {
    if (MyCounter.#fragment === null) {
      const template = document.getElementById("my-counter");
      MyCounter.#fragment = document.adoptNode(template.content);
    }
  
    return MyCounter.#fragment.cloneNode(true);
  }
```

The `#createView` method checks if the template has been adopted by the class. If not, it adopts the template into the document. Then, it clones this template to create a view for the instance. This process is a critical performance optimisation, avoiding unnecessary operations on the DOM.

## Component Registration

```js
customElements.define("my-counter", MyCounter);
```

Finally, `MyCounter` is registered as a custom element with `customElements.define`, allowing developers to use `<my-counter>` tags directly in their HTML, just like any other standard element.

## Conclusion

`MyCounter` demonstrates the very basic and low-level characteristics of creating and updating a custom element. I hope you enjoyed reading this - I'll be posting more as I continue learning.