Introduction to Web Components — Part I Custom Elements

Alex Korzhikov
ITNEXT
Published in
8 min readAug 15, 2019

--

The HTML specification describes a number of different tags which can be used to structure a Web page or an application. These tags define elements which have either functional or User Interface intent. Both head and body tags are traditional for HTML layout. Different meta definitions and dependencies are declared inside the head section, with the help of link, script, title, and meta tags, etc. On the other hand, the body element defines the document's structure with elements like:

  • div and span which sometimes are used as the base for block or inline displayed elements respectively
  • p describes a paragraph with any text inside
  • h1 to h6 tags define different heading levels in the document
  • button can act as a user action, i.e. inside a form or for a navigation purpose
  • the form tag helps to locate fields for entering user’s data
  • select and input elements are used for the form field’s description. Adding an attribute for these tags can define validation rules, size, or possible types of input data, like number, date or password. Since the HTML5 standard was introduced, even more specific tags became available for developers, like:
  • audio and video elements locate and provide multimedia content
  • section, article, nav, header, footer and many other tags outline the structure, adding more semantics for the document. The list is not full, it just shows the different possibilities available for FrontEnd developers to structure the Web page.

What if a project needs an element which behaves differently?

It could be, for example, a tab element to demonstrate different content regarding the selected item or an expand element to show extra content for text, etc. Modern libraries and frameworks like Vue, React, and Angular introduces many abstractions around DOM to simplify the concept and usage of custom elements. All of them decompose an application in terms of components. They define rules of how components can be created, inserted into the document, how they interact with each other, and how they are rendered. In fact, there is a Virtual DOM algorithm used both in Vue and React libraries’ internals which creates a structure living in parallel with a real HTML structure. Each time the framework is about to update the view, first the Virtual DOM implementation compares what needs to be updated before applying any changes to the real DOM. Older libraries like jQuery are solving that situation with JavaScriptlogic applied to the HTML structure. Usually, the element is served as an entry point for the jQuery plugin, which wraps it, adds additional elements, binds event listeners, and attaches a business logic. The document’s structure in the meantime doesn't always represents the developer's intent - the magic happens inside the JavaScript library. However that approach works for smaller sites, the bigger page grows the harder it becomes to maintain and extend it.

Nowadays there is a native way to achieve the element’s customization — Web Components

The Web Components specification is a common name for Web standards which purpose is to represent a browser-based application as a hierarchy of reusable elements. The idea is to give a developer an ability to define their own component where he or she can encapsulate the state, the behavior, styles, etc. Components can communicate with each other and are "natural" for the browser environment. Elements can be added into a page via the HTML declaration so the document's structure is represented more consistently.

Web Components

Custom Elements

The Custom Elements specifications allow developers to define their own elements, almost with no difference with built-in ones. In fact the process can even be inverted and in the end native elements are meant to be Custom Elements, defined inside the browser engine in a similar way like a FrontEnd developer can do in an application.

<select>
<option value="1">1</option>
<option value="2">2</option>
</select>
<!-- What if we want multiple select? --><multiple-select>
<option value="1">1</option>
<option value="2">2</option>
</multiple-select>

The multiple-select element doesn't exist in the browser, but it would be useful to have one. So the Custom Element can be defined in JavaScript code inside the page.

<script>
class HelloWorldElement extends HTMLElement {
connectedCallback() {
this.textContent = "Hello World"
}
}
customElements.define('hello-world-element', HelloWorldElement);
</script>
<hello-world-element></hello-world-element>

There is a new HelloWorldElement class definition in the code. It is inherited from the base HTMLElement class and adds the text after the initialization. The connectedCallback is a lifecycle hook, which is executed when an element is added to the document. It is a part of the lifecycle system, which is described later. After the class itself is defined the important thing happens - customElements.define() is called both with a tag name and the HelloWorldElement class. the HelloWorldElement is registered inside the customElements - the registry for all the elements on a page. The customElements is the instance of CustomElementRegistry class, exposed for every HTML document. That is the unique object per a page, or to be precise per a page’s window instance. The specification relates it to an HTMLElement class specificity per the window namespace as well. After the definition is done it is possible to create the HelloWorldElement element in any potential way.

Either with the JavaScript document createElement() method or by calling the recently created class’ constructor followed by attaching an element to the DOM structure.

// use createElement
const helloWorld = document.createElement('hello-world-element')
document.body.appendChild(helloWorld)
// use new
const newHelloWorld = new HelloWorldElement()
document.body.appendChild(newHelloWorld)

It is also possible to declare it inside the HTML markdown as well.

<hello-world-element></hello-world-element>

In any chosen way the element will appear on the page and will behave accordingly to it’s definition.

Upgrade

In the latest example, the HTML and JavaScript code is separated, so the elements’ declaration can occur before, after or even during the script evaluation. A non-custom element can become a custom element at any time. That is covered by the specification and that process is called the upgrading procedure. The upgrading process is triggered normally when the element is added to DOM. However, it can be imperatively done by the customElements.upgrade() method even when the element is out of the document structure.

Attributes Example

It is possible to describe an attribute setters / getters behaviors for the HTML syntax compliance. The following code demonstrates this process. When added attributeChangedCallback and connectedCallback methods are executed, they check the current element’s attribute value and sync the internal and external states - it's attributes and properties.

class HelloWorldElement extends HTMLElement {
static get observedAttributes() {
return ['name']
}
attributeChangedCallback(name, oldValue, newValue) {
this._name = newValue
}
connectedCallback() {
this.name = this.getAttribute('name') || 'World'
}
get name() {
return this._name
}
set name(name) {
this.setAttribute('name', name)
this.render()
}
render() {
this.textContent = `Hello ${this.name}`
}
}
customElements.define('hello-world-element', HelloWorldElement)

Note that there should be defined static method observedAttributes() for subscribing for an attribute change event. It should return an array of names to listen for. When the element is declared like

<hello-world-element name="Alex"></hello-world-element>

the text Hello Alex will be printed. In the other way around

<hello-world-element></hello-world-element>

will output Hello World as a default value.

Extend Existing Element

It is quite hard to mimic existing browser elements. For example, the HTML button has a set of behaviors, that are not available for new custom elements:

  • it is focused when the tab keyboard event happens. That can be achieved by adding a tabindex attribute to the new element.
  • the button can be activated or “pushed” both by the mouse and the keyboard. Inside a form, the button can submit data. Additional event listeners are required to add this ability.
  • it has an internal disabled state, which can be implemented by JavaScript code.
  • it supports accessibility-related features, like aria-label, role, etc. And it's just a button. It gets even more difficult with interactive elements, like input, form or img. It is easy to forget any of those built-in features and make a user unhappy because of undesired and unexpected behavior. The Custom Elements specification proposes a way to mitigate the difference between built-in and new elements.

So far all the examples showed the creation of new kinds of elements, which were inherited from the base HTMLElement class. These type of elements is called "autonomous custom elements".

The way to reuse more than just a base HTMLElement class is to specify the extends flag while registering the new element’s type inside the customElements. Any elements that are using the extends option are named "customized built-in elements".

Any built-in element can be customized by extending its JavaScript class and using special definition arguments. In the following example, the original button element is inherited by a new MyButton class.

class MyButton extends HTMLButtonElement {
constructor() {
super()
// ...
}
}
customElements.define('my-button', MyButton, { extends: 'button' })
document.createElement('button', { is: 'my-button' })

Of course it is possible to declare an element in HTML as well.

<button is="my-button">Click Me!</button>

Note that the is attribute’s presence is necessary for a customized built-in element's creation.

Lifecycle

The lifecycle is a regular term when discussing components in the context of FrontEnd frameworks and libraries. It describes how the component is created, how it is modified and what happens during this process.

It is quite usual to have events bound to one or another phase of the component’s life. And those events are attached to hooks or reactions — methods, which will be invoked whenever such an event happens.

When an event happens, the corresponding method, if it is set on the custom element’s class, is called. Reactions defined for any Custom Element are:

  • The upgrade reaction, which includes the element's constructor call.
  • The element‘s connectedCallback is executed when the element is added to the document or connected in other words.
  • The disconnectedCallback is the opposite to the connected event's reaction and occurs when the element disconnects or is removed from DOM.
  • The adopted event happens when the element changes its parent document.
  • Finally, the attributeChangedCallback is called whenever the element's attributes are changed, including addition and removal. When it is called, the callback receives an attribute name, old and new values as arguments in this order.
class CustomElement extends HTMLElement {
constructor() {
super()
console.log('constructor')
}
connectedCallback() {
this.textContent = "Hello Custom Element"
}
disconnectedCallback() {
console.log('disconnectedCallback')
}
adoptedCallback() {
console.log('adoptedCallback')
}
attributeChangedCallback() {
console.log('attributeChangedCallback')
}
static get observedAttributes() {
return ['test']
}
}
customElements.define('hello-custom-element', CustomElement)
<hello-custom-element test="test"></hello-custom-element>

Note that the constructor()'s body starts with a super() call. That executes a parent's class constructor() so the initialization can proceed. The constructor()'s body should not modify or use the element or it's attributes. It's purpose is to define the initial elements' state and attach all needed event listeners. All the extra work should be deferred to connectedCallback.

Summary

In the first part of this Web Components series, we’ve introduced basic concepts of the Custom Elements specification. We’ve overviewed how to define, use, upgrade, and use lifecycle hooks for new elements. The next article is about Shadow DOM and HTML Template usage.

Have a nice coding and hope to see you in comments!

--

--

Software engineer, instructor, mentor, and author of technical materials #JavaScript