Web Components & Custom Elements
Tags: webdev, javascript, webcomponents, beginners
Published: 2024-01-01 07:04:31

Those familiar with React or any other javascript framework, are already aware of the component based architecture. You break the UI into re-usable pieces of code and stitch them together when required.

Custom elements in HTML are a way to extend native HTML elements. Javascript frameworks simulate the behavior of components in a web page whereas custom elements provide a native HTML-ly way to do so. A web component uses custom elements along with other techniques such as the shadow DOM.

## Types Of Custom Elements

- Autonomous custom elements
- Customized built-in elements

Autonomous custom elements extend the generic [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) class. On the other hand, a customized custom element extends a specific HTML elements' class and builds on top of existing functionality. For example, if you want a custom anchor element, you can extend the [HTMLAnchorElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement).

## Defining Custom Elements

To define a custom element, we need to create a javascript class extending the native HTMLElement class. Try creating the below custom element in a codepen:

```js
class Demo extends HTMLElement {
  constructor() {
    super()
  }
  
  connectedCallback() {
    this.textContent = "hello"
  }
}

customElements.define("demo", Demo)
```

And then invoking it in HTML:

```html
<demo></demo>
```

It won't let you create it. Why? See the error.

```console
Uncaught SyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "demo" is not a valid custom element name
```

This is not a bug, this is intentionally done to separate custom elements from native HTML elements. Custom elements must contain a hyphen in their name to make custom elements recognizable and distinct from HTML elements.

So now, if you change it to something like `demo-webc` and change the class name to `DemoWebC`, it works.

```js
class DemoWebC extends HTMLElement {
  constructor() {
    super()
    this.customProperty = "custom"
  }
  
  connectedCallback() {
    this.textContent = "hello"
  }
}

// two arguments: tag name, class name
customElements.define("demo-webc", DemoWebC)
```

It always recommended to call the `super()` method first in the constructor as it initializes default properties of the `HTMLElement` class by invoking its constructor.

The `connectedCallback()` method is for detecting when the element is loaded into the page. There's also a method named `disconnectedCallback()` which detects if the element is removed from the page. A third method name `adoptedCallback()` says that the element has moved to a new page.

You can define custom properties inside the constructor and use them as custom attributes in your element.

```js
constructor() {
    super()
    this.customProperty = {
        name: "data-custom",
        value: "custom value"
    }

    connectedCallback() {
        this.textContent = "hello"
        this.setAttribute(this.customAttribute.name, this.customAttribute.value)
    }
}
```

But what if you need to modify the functionality on attribute's value change? That's where `attributeChangedCallback()` method comes into action. In order to see it in action, you need to first define a static `observedAttributes` class property and set it to an array of all the attributes you want to keep track of.

The `attributeChangedCallback()` fires if those attributes mentioned in the static `observedAttributes` property change. Note that if the attribute is already present when the custom element loads, this method is fired at that time too.

```js
static observedAttributes = ["data-custom"]

constructor() {
    super()
}

attributeChangedCallback(name, old, newValue) {
    console.log(name, old, newValue)
}
```

Once you are done with building a custom element, you must register it by using the `define()` method. It's callable on the `customElements` global object (`window.customElements`) which is a registry of custom elements.

```js
customElements.define("custom-element-name", ClassName, options)
```

This was all for defining a customized autonomous custom element. What about extending only an anchor HTML element?

For that, instead of extending the `HTMLElement` class, extend the `HTMLAnchorElement` class. And specify which type of HTML element it extends with the `extends` option.

```js
class DemoAnchor extends HTMLAnchorElement {
  constructor() {
    super()
  }
  
  connectedCallback() {
    this.textContent = "syntackle.live"
    this.href = "https://syntackle.live"
  }
}

customElements.define("demo-anchor", DemoAnchor, { extends: "a" })
```

You can't use this element like `<demo-anchor>` because it's not an autonomous element, instead you can use it like this:

```html
<a is="demo-anchor"></a>
```

## Web Components

Web components are more than just custom elements. They sometimes also involve a shadow DOM. A *"shadow"* DOM, as the name suggests, is a sub-DOM tree for HTML elements. It is mainly used for encapsulation and restricting styles up to the web component only.

### Shadow DOM

To create a shadow DOM, attach it to a host, in our case the custom element itself is a host to the shadow DOM. However, the shadow DOM can only be attached to a custom element or [these built-in elements mentioned in the HTML spec](https://dom.spec.whatwg.org/#dom-element-attachshadow).

> You *can* access elements outside the shadow DOM from inside the shadow DOM.

```js
class DemoWebC extends HTMLElement {
  constructor() {
    super()
  }

  connectedCallback() {
    const shadow = this.attachShadow({mode: "open"})

    const style = document.createElement("style")
    style.textContent = `p { color: blue; }`
    shadow.appendChild(style)

    const text = document.createElement("p")
    text.textContent = "hello"
    shadow.appendChild(text)
  }
}

customElements.define("demo-webc", DemoWebC)
```

Shadow DOM has two modes: `open` and `closed`. Open means external elements in the page can modify the contents of the shadow DOM by using `shadowRoot` property. In the `closed` mode, the shadow DOM is not accessible from outside using the `shadowRoot` property as it is `null` in this case.

Try doing this on a closed shadow DOM custom element:

```js
console.log(document.querySelector("demo-webc").shadowRoot)
```

It returns `null`.

> ***Templates*** and ***slots*** are extremely useful when building complex custom elements or web components. Diving deep into them is out of the scope of this article, but here are some good resources for them:
>
> - [Using templates and slots - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
> - [Shadow DOM slots, composition](https://javascript.info/slots-composition)
> - [Working with Slots and Web Components](https://www.raymondcamden.com/2022/10/13/working-with-slots-and-web-components)

### Styling Shadow DOM

The shadow DOM can be styled either by:

- Constructing a [`CSSStyleSheet`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) object, inserting CSS in it using `replaceSync()` and attaching it to the shadow DOM using the `adoptedStyleSheets` property.

```js
const shadowDOM = this.attachShadow({mode: "open"})
const styleSheet = new CSSStyleSheet()
styleSheet.replaceSync(`p { color: blue; }`)
shadowDOM.adoptedStyleSheets = [styleSheet]
```

- Declaring styles using a [`<template>`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM#adding_style_elements_in_template_declarations).

```html
<template id="custom">
    <head>
        <style>p { color: blue; }</style>
    </head>
    <p>Web Component</p>
</template>
```

```js
const shadowDOM = this.attachShadow({ mode: "open" })
const template = document.querySelector("#custom")
shadowDOM.appendChild(template.content.cloneNode(true))
```

- Simply creating a `style` tag and inserting CSS as text in it.

```js
const style = document.createElement("style")
style.textContent = `p { color: blue; }`
shadow.appendChild(style)
```

## Creating Your Own Web Component

The first web component shown below is a custom button element which opens a `dialog` element. And the second web component involves a shadow DOM to pretty print JSON string in HTML.

Similarly, you can create your own custom elements and use them anywhere you want.

{% codepen https://codepen.io/seekertruth/pen/LYaYqmo %}