Web Components

Neil HaddleyFebruary 10, 2022

A set of web technologies that allow users to create HTML elements.

History

In 1998 Microsoft Internet Explorer 5.5 added support for HTML Components.

In 2001 Mozilla introduced XBL.

In 2007 Mozilla released XBL 2.

In 2016 Chrome and Opera added support for Web Components.

howto-tooltip

The aria-describedby attribute lists the ids of the elements that describe the object. It is used to establish a relationship between widgets or groups and the text that describes them.

In the google chrome labs github repository there is a Web Component that creates a howto-tooltip element.

The howto-tooltip element only displays the "described by" content when a user is focused on the described element.

demo

Move to and away from the text box below.

If Web Components are supported by your browser the tooltip should appear and disappear.

============

Favourite type of cheese:

Help I am trapped inside a tooltip message

============

The howto-tooltip Web Component is created using a JavaScript class and defined with a window.customElements.define() call.

The JavaScript class has a constructor, a _show method, a _hide method and an implementation for the connectedCallback and disconnectedCallback lifecycle methods.

The constructor uses bind to ensure that the _show and _hide methods can show or hide "this" howto-tooltip element.

The lifecycle methods add or remove event listeners to the described elements.

Notice that if the HTML is accessed by a Web Browser that does not support Web Components (and no "polyfill" has been included) the user is still able to read the tooltip text.

template

An HTML template is a section of HTML that can be stamped out multiple times on multiple HTML pages.

Slots are used to identify places in the HTML template where other content can be grafted.

Consider this template from the Web Components Are Easier Than You Think article

<body>

<template id="my-paragraph">

<p>My paragraph</p>

</template>

</body>

my-paragraph template

my-paragraph template

Using a template with JavaScript

Adding a template element to a web page does not result in anything being displayed (see above).

We can use JavaScript to apply the template.

<script>

let template = document.getElementById('my-paragraph');

let templateContent = template.content;

document.body.appendChild(templateContent.cloneNode(true))

document.body.appendChild(templateContent.cloneNode(true))

document.body.appendChild(templateContent.cloneNode(true))

</script>

Using an HTML template

Using an HTML template

Using a template in a Web Component

An easier way to apply a template is to create a Web Component.

<script>

customElements.define("my-element",

class extends HTMLElement {

constructor() {

super();

let template = document.getElementById("my-paragraph");

let templatecontent = template.content;

const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(templatecontent.cloneNode(true));

}

});

</script>

JavaScript only

...and this can be re-written without using the <template> tag.

Slots

Slots are a way to customize a template.

If a developer wants to add more than one slot to a single template they need to provide ids.

Consider these examples:

Hello World!

<template>

<p>Hello <slot>World</slot>!</p>

</template>

and

<template>

<p><slot name="greeting">Hello</slot> <slot name="name">World</slot>!</p>

</template>

Web Components with slots

The example below shows how a Web Component can be created with support for slots.

Web Component with slots

Web Component with slots

Properties

Values can also be passed to Web Components using HTML element properties

Events

Web Component events can be bubbled up to parent elements.

Web Components can dispatch custom events.

The click event is bubbled up. The custom tick events are dispatched.

The click event is bubbled up. The custom tick events are dispatched.

Are Web Components the future?

In her article Anna Monus explains:

These days, web components are a divisive topic. They were once expected to revolutionize frontend development, but they’re still struggling to achieve industrywide adoption. Some developers say web components have already died, while others think they’re the future of web development.

...

UI libraries, such as React, Vue, and Angular, serve the same purpose as web components: they make component-based frontend development possible. Even though they’re not native to web browsers (you have to add the libraries separately while web components use web APIs built into the browser, such as DOM and CustomElementRegistry), they have a huge ecosystem, good documentation, and many developer-friendly features.

https://blog.logrocket.com/what-happened-to-web-components/

React integration

In his article Caleb Williams explains:

*As of the time of this writing, React recently released version 17. The React team had initially planned to release improvements for compatibility with custom elements; unfortunately, those plans seem to have been pushed back to version 18.

Until then it will take a little extra work to use all the features custom elements offer with React. Hopefully, the React team will continue to improve support to bridge the gap between React and the web platform.*

https://css-tricks.com/3-approaches-to-integrate-react-with-custom-elements/

html embedded above

TEXT
1<label for="cheese">Favourite type of cheese: </label>
2<input id="cheese" aria-describedby="tp2"/>
3<howto-tooltip id="tp2">Help I am trapped inside a tooltip message</howto-tooltip>

howtoTooltip.js

TEXT
1class HowtoTooltip extends HTMLElement {
2
3    constructor() {
4        super();
5
6        this._show = this._show.bind(this);
7        this._hide = this._hide.bind(this);
8    }
9
10    connectedCallback() {
11        this._hide();
12
13        this._target = document.querySelector('[aria-describedby=' + this.id + ']');
14        if (!this._target)
15            return;
16
17        this._target.addEventListener('focus', this._show);
18        this._target.addEventListener('blur', this._hide);
19        this._target.addEventListener('mouseenter', this._show);
20        this._target.addEventListener('mouseleave', this._hide);
21    }
22
23    disconnectedCallback() {
24        if (!this._target)
25            return;
26
27        this._target.removeEventListener('focus', this._show);
28        this._target.removeEventListener('blur', this._hide);
29        this._target.removeEventListener('mouseenter', this._show);
30        this._target.removeEventListener('mouseleave', this._hide);
31        this._target = null;
32    }
33
34    _show() {
35        this.hidden = false;
36    }
37
38    _hide() {
39        this.hidden = true;
40    }
41}
42
43
44window.customElements.define('howto-tooltip', HowtoTooltip)
45
46customElements.whenDefined('howto-tooltip').then(() => {
47    console.log('howto-tooltip ready!');
48});

template.html

TEXT
1<body>
2
3    <template id="my-paragraph">
4        <p>My paragraph</p>
5    </template>
6
7
8    <script>
9
10        customElements.define("my-element",
11            class extends HTMLElement {
12                constructor() {
13                    super();
14                    let template = document.getElementById("my-paragraph");
15                    let templatecontent = template.content;
16                    const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(templatecontent.cloneNode(true));
17                }
18            });
19
20    </script>
21
22    <my-element></my-element>
23    <my-element></my-element>
24    <my-element></my-element>
25
26</body>

template.html

TEXT
1<body>
2
3    <script>
4
5        const template = document.createElement('template');
6        template.innerHTML = `
7            <p>My paragraph</p>
8        `
9
10        customElements.define("my-element",
11            class extends HTMLElement {
12                constructor() {
13                    super();
14                    let templatecontent = template.content;
15                    const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(templatecontent.cloneNode(true));
16                }
17            });
18
19    </script>
20
21    <my-element></my-element>
22    <my-element></my-element>
23    <my-element></my-element>
24
25</body>

template.html

TEXT
1<body>
2
3    <script>
4
5        const template = document.createElement('template');
6        template.innerHTML = `
7            <p><slot name="greeting">Hello</slot> <slot name="name">World</slot>!</p>
8        `
9
10        customElements.define("my-element",
11            class extends HTMLElement {
12                constructor() {
13                    super();
14                    let templatecontent = template.content;
15                    const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(templatecontent.cloneNode(true));
16                }
17            });
18
19    </script>
20
21    <my-element></my-element>
22    <my-element><span slot="name">Neil</span></my-element>
23    <my-element><span slot="name">Neil</span><span slot="greeting">Welcome</span></my-element>
24
25</body>

template.html

TEXT
1<body>
2
3    <script>
4
5        const template = document.createElement('template');
6        template.innerHTML = `
7            <p><slot>Hello</slot> <span id="name">World</span>!</p>
8        `
9
10        customElements.define("my-element",
11            class extends HTMLElement {
12                constructor() {
13                    super();
14                    let templatecontent = template.content;
15                    const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(templatecontent.cloneNode(true));
16                }
17                static get observedAttributes() {
18                    return ['name'];
19                }
20
21                attributeChangedCallback(name, oldValue, newValue) {
22                    if (name == 'name') {
23                        const span = this.shadowRoot.querySelector('#name')
24                        if (newValue) {
25                            span.innerText = newValue
26                            return
27                        } 
28                        span.innerText = "World"
29                    }
30                }
31            });
32
33
34    </script>
35
36    <my-element></my-element>
37    <my-element name="Neil"></my-element>
38    <my-element name="Neil"><span slot>Welcome</span></my-element>

template.html

TEXT
1<body>
2
3    <script>
4
5        const template = document.createElement('template');
6        template.innerHTML = `
7            <p><slot>Hello</slot> <span id="name">World</span>!</p>
8        `
9
10        customElements.define("my-element",
11            class extends HTMLElement {
12                constructor() {
13                    super();
14                    let templatecontent = template.content;
15                    const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(templatecontent.cloneNode(true));
16                }
17
18                static get observedAttributes() {
19                    return ['name'];
20                }
21
22                attributeChangedCallback(name, oldValue, newValue) {
23                    if (name == 'name') {
24                        const span = this.shadowRoot.querySelector('#name')
25                        if (newValue) {
26                            span.innerText = newValue
27                            return
28                        }
29                        span.innerText = "World"
30                    }
31                }
32
33                connectedCallback() {
34                    this._interval = setInterval( () => {
35                        console.log('.')
36                        this._tick()
37                    }, 50000);
38                }
39
40                disconnectedCallback() {
41                    clearInterval(this._interval);
42                }
43
44                _tick() {
45                    const tickEvent = new CustomEvent("tick", {
46                        bubbles: true,
47                        cancelable: false,
48                        composed: true
49                    })
50                    this.dispatchEvent(tickEvent);
51                }
52
53            })
54
55    </script>
56
57    <my-element id='first'></my-element>
58    <my-element id='second' name="Neil"></my-element>
59    <my-element name="Neil"><span slot>Welcome</span></my-element>
60
61    <script>
62
63        document.querySelector('#first').addEventListener("tick", (e) => {
64            console.log('tick');
65            console.log(e);
66        });
67
68        document.querySelector('#first').addEventListener("click", (e) => {
69            console.log('click');
70            console.log(e);
71        });
72
73        document.querySelector('#second').addEventListener("tick", function (e) {
74            console.log('tock');
75            console.log(e);
76        });
77
78    </script>
79
80</body>