Introduction to Web Components — Part I Custom Elements
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
andspan
which sometimes are used as the base forblock
orinline
displayed elements respectivelyp
describes a paragraph with any text insideh1
toh6
tags define different heading levels in the documentbutton
can act as a user action, i.e. inside aform
or for anavigation
purpose- the
form
tag helps to locate fields for entering user’s data select
andinput
elements are used for theform
field’s description. Adding anattribute
for these tags can define validation rules, size, or possible types of input data, like number, date or password. Since theHTML5
standard was introduced, even more specific tags became available for developers, like:audio
andvideo
elements locate and provide multimedia contentsection
,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 forFrontEnd
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 JavaScript
logic 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.
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 thetab
keyboard event happens. That can be achieved by adding atabindex
attribute to the new element. - the
button
can be activated or “pushed” both by themouse
and thekeyboard
. Inside a form, thebutton
can submit data. Additional event listeners are required to add this ability. - it has an internal
disabled
state, which can be implemented byJavaScript
code. - it supports accessibility-related features, like
aria-label, role
, etc. And it's just abutton
. It gets even more difficult with interactive elements, likeinput
,form
orimg
. It is easy to forget any of those built-in features and make a user unhappy because of undesired and unexpected behavior. TheCustom 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'sconstructor
call. - The element‘s
connectedCallback
is executed when the element is added to the document orconnected
in other words. - The
disconnectedCallback
is the opposite to theconnected
event's reaction and occurs when the elementdisconnects
or is removed fromDOM
. - 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!