[We advise you to read] Other 19 parts of the cycle
We present to your attention a translation of 19 articles from the
SessionStack series of materials on the features of various JavaScript ecosystem mechanisms. Today we will talk about the standard Custom Elements - the so-called "custom elements". We will talk about what tasks they allow to solve, and how to create and use them.
Overview
In one of the previous articles in this series, we talked about the
Shadow DOM and some other technologies that are part of a larger phenomenon — web components. Web components are designed to give developers the ability to extend standard HTML capabilities by creating compact, modular and reusable elements. This is a relatively new W3C standard, which manufacturers of all leading browsers have already noticed. It can be found in production, although, of course, while his work is provided by polyfills (we'll talk about them later).
As you may already know, browsers provide us with several critical tools for developing websites and web applications. We are talking about HTML, CSS and JavaScript. HTML is used to structure web pages, thanks to CSS they give a nice appearance, and JavaScript is responsible for interactive features. However, before the advent of web components, it was not so easy to link actions implemented by JavaScript tools with the HTML structure.
')
As a matter of fact, here we look at the basis of web components - custom elements (Custom Elements). If you talk about them in a nutshell, the API designed to work with them allows the programmer to create custom HTML elements with JavaScript logic embedded in them and styles described by CSS. Many confuse custom elements with Shadow DOM technology. However, these are two completely different things that, in fact, complement each other, but are not interchangeable.
Some frameworks (such as Angular or React) try to solve the same problem that custom elements solve by introducing their own concepts. Custom elements can be compared with Angular directives or with React components. However, custom elements are a standard browser feature; you do not need anything other than regular JavaScript, HTML, and CSS to work with them. Of course, this does not allow us to say that they are a substitute for ordinary JS frameworks. Modern frameworks give us much more than just the ability to imitate the behavior of user elements. As a result, we can say that both frameworks and custom elements are technologies that can be used together to solve web development tasks.
API
Before we continue, let's see what opportunities the API provides for working with custom elements. Namely, we are talking about the global
customElements
object, which has several methods:
- The
define(tagName, constructor, options)
method allows you to define (create, register) a new custom element. It takes three arguments - the name of the tag for the custom element, which corresponds to the rules for naming such elements, the class declaration and an object with parameters. Currently, only one parameter is supported - extends
, which is a string specifying the name of an inline element that is planned to be expanded. This feature is used to create special versions of standard elements. - The
get(tagName)
method returns a custom element constructor, provided that this element is already defined, otherwise it returns undefined
. It takes one argument — the tag name of the custom item. - The
whenDefined(tagName)
method returns a promise that is resolved after the user element is created. If an item is already defined, this promise is resolved immediately. A promis is rejected if the tag name passed to it is not a valid custom element tag name. This method takes the tag name of the custom item.
Creating custom items
Creating custom elements is easy. To do this, you need to do two things: create a class declaration for the element that must extend the
HTMLElement
class and register this element with the selected name. Here's what it looks like:
class MyCustomElement extends HTMLElement { constructor() { super();
If you do not want to pollute the current scope, you can use an anonymous class:
customElements.define('my-custom-element', class extends HTMLElement { constructor() { super();
As you can see from the examples, the user element is registered using the
customElements.define(...)
method already familiar to you.
Problems that Custom Elements Solve
Let's talk about the problems that allow us to solve custom elements. One of them is to improve the structure of the code and eliminate what is called “soup from div tags” (div soup). This phenomenon is a very common code structure in modern web applications, in which there are many
div
elements nested in each other. Here is what it might look like:
<div class="top-container"> <div class="middle-container"> <div class="inside-container"> <div class="inside-inside-container"> <div class="are-we-really-doing-this"> <div class="mariana-trench"> … </div> </div> </div> </div> </div> </div>
Such HTML code is used for justifiable reasons - it describes the device of the page and provides its correct output to the screen. However, this worsens the readability of the HTML code and complicates its maintenance.
Suppose we have a component that looks like the one shown in the following figure.
Component appearance
When using the traditional approach to the description of such things this component will correspond to the following code:
<div class="primary-toolbar toolbar"> <div class="toolbar"> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-undo"> </div> </div> </div> </div> </div> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-redo"> </div> </div> </div> </div> </div> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-print"> </div> </div> </div> </div> </div> <div class="toolbar-toggle-button toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-paint-format"> </div> </div> </div> </div> </div> </div> </div>
Now imagine that we could, instead of this code, use this component description:
<primary-toolbar> <toolbar-group> <toolbar-button class="icon-undo"></toolbar-button> <toolbar-button class="icon-redo"></toolbar-button> <toolbar-button class="icon-print"></toolbar-button> <toolbar-toggle-button class="icon-paint-format"></toolbar-toggle-button> </toolbar-group> </primary-toolbar>
I'm sure everyone will agree that the second code fragment looks much better. This code is easier to read, easier to maintain, it is understandable to both the developer and the browser. It all comes down to the fact that it is simpler than the one in which there are many nested
div
tags.
The next problem that can be solved with custom elements is code reuse. The code that developers write should be not only working, but also supported. Reusing code, as opposed to constantly writing the same constructs, improves the ability to support projects.
Here is a simple example that will allow you to better understand this idea. Suppose we have the following element:
<div class="my-custom-element"> <input type="text" class="email" /> <button class="submit"></button> </div>
If there is always a need for it, then, with the usual approach, we will have to write the same HTML code again and again. Now imagine that you need to make a change in this code that should be reflected wherever it is used. This means that we need to find all the places where this fragment is used, and then make the same changes everywhere. It is long, hard and fraught with mistakes.
It would be much better if we could, where this element is needed, just write the following:
<my-custom-element></my-custom-element>
However, modern web applications are much more than static HTML. They are interactive. The source of their interactivity is javascript. Usually, to provide such opportunities, they create certain elements, then they connect event listeners to them, which allows them to react to the user's actions. For example, they can react to clicks, the mouse hovering over them, dragging them around the screen, and so on. Here's how to connect to the element the event listener that occurs when you click on it with the mouse:
var myDiv = document.querySelector('.my-custom-element'); myDiv.addEventListener('click', _ => { myDiv.innerHTML = '<b> I have been clicked </b>'; });
And here is the HTML code for this element:
<div class="my-custom-element"> I have not been clicked yet. </div>
By using the API to work with custom elements, all this logic can be incorporated into the element itself. For comparison, the code for declaring a custom element that includes an event handler is shown below:
class MyCustomElement extends HTMLElement { constructor() { super(); var self = this; self.addEventListener('click', _ => { self.innerHTML = '<b> I have been clicked </b>'; }); } } customElements.define('my-custom-element', MyCustomElement);
And this is how it looks in the HTML code of the page:
<my-custom-element> I have not been clicked yet </my-custom-element>
At first glance it may seem that to create a custom element requires more lines of JS-code. However, in real applications it is rarely the case that such elements would be created only in order to use them only once. Another typical phenomenon in modern web applications is that most of the elements in them are created dynamically. This leads to the need to support two different scenarios for working with elements - situations when they are added to the page dynamically with JavaScript tools, and situations when they are described in the original HTML structure of the page. Thanks to the use of custom elements work in these two situations is simplified.
As a result, if we summarize this section, we can say that user elements make the code clearer, simplify its support, contribute to splitting it into small modules that include all the necessary functionality and are suitable for reuse.
Now that we have discussed general issues of working with custom elements, let's talk about their features.
Requirements
Before you begin developing your own custom elements, you should be aware of some of the rules to follow when creating them. Here they are:
- The component name must include a hyphen (
-
symbol). This allows the HTML parser to distinguish between embedded and custom elements. In addition, this approach ensures the absence of name collisions with embedded elements (both with those that exist now and those that appear in the future). For example, the actual name of the custom element is >my-custom-element<
, and the names >myCustomElement<
and <my_custom_element>
are inappropriate. - It is forbidden to register the same tag more than once. Attempting to do this will cause the browser to issue a
DOMException
error. Custom members cannot be overridden. - Custom tags cannot be self-closing. The HTML parser supports only a limited set of standard self-closing tags (for example,
<img>
, <link>
, <br>
).
Opportunities
Let's talk about what you can do with custom elements. If you briefly answer this question, it turns out that you can do a lot of interesting things with them.
One of the most noticeable features of custom elements is that the class declaration of an element refers to the DOM element itself. This means that you can use the this keyword in your ad to connect event listeners, access properties, child nodes, and so on.
class MyCustomElement extends HTMLElement {
This, of course, makes it possible to write new data to the child nodes of the element. However, it is not recommended to do this, as this may lead to unexpected behavior of the elements. If you imagine that you are using elements that are developed by someone else, then you will surely be surprised if your own markup placed in the element is replaced with something else.
There are several methods that allow you to execute code at certain points in an element's life cycle.
- The
constructor
method is called once, when creating or “updating” (upgrading) an element (we will discuss this later). Most often it is used to initialize the state of an element, to connect event listeners, create a Shadow DOM, and so on. Do not forget that in the constructor you always need to call super()
. - The
connectedCallback
method is called each time an element is added to the DOM. It can be used (and it is recommended to use it) in order to postpone any actions until the element is on the page (for example, you can postpone loading some data). - The
disconnectedCallback
method is called when an element is removed from the DOM. It is usually used to free up resources. Note that this method is not called if the user closes the browser tab with the page. Therefore, do not rely on it when you need to perform some particularly important actions. - The
attributeChangedCallback
method is called when an element attributeChangedCallback
is added, deleted, updated, or replaced. In addition, it is called when the element is created by the parser. Note, however, that this method applies only to the attributes that are listed in the observedAttributes
property. - The
adoptedCallback
method adoptedCallback
called when the document.adoptNode(...)
method is used to move a node to another document.
Please note that all the above methods are synchronous. For example, the
connectedCallback
method is called immediately after an element is added to the DOM, and the rest of the program is waiting for this method to finish.
Property Reflection
Embedded HTML elements have one very convenient feature: property reflection. Thanks to this mechanism, the values ​​of some properties are directly reflected in the DOM as attributes. Let's say this is typical for the
id
property. For example, perform the following operation:
myDiv.id = 'new-id';
Relevant changes will also affect DOM:
<div id="new-id"> ... </div>
This mechanism works in the opposite direction. It is very useful as it allows you to configure elements declaratively.
Custom elements have no such built-in capability, but you can implement it yourself. In order for some properties of custom elements to behave in a similar way, you can configure their getters and setters.
class MyCustomElement extends HTMLElement {
Expansion of existing elements
The User Element API allows you to not only create new HTML elements, but also extend existing ones. Moreover, we are talking about standard elements, and custom. This is done by using the
extends
when declaring a class:
class MyAwesomeButton extends MyButton {
Extended standard elements are also called “customized built-in elements” (customized built-in element).
It is recommended to make it a rule to always expand existing elements, and to do this progressively. This will allow you to retain in the new elements the capabilities that were implemented in the previously created elements (that is, properties, attributes, functions).
Please note that now custom built-in elements are supported only in Chrome 67+. This will appear in other browsers, however, it is known that the Safari developers have decided not to implement this feature.
Update items
As already mentioned, the
customElements.define(...)
method is used to register custom elements. However, registration cannot be called the action that needs to be performed first. The registration of a user element can be postponed for some time, moreover, this time can come even when the element is already added to the DOM. This process is called the upgrade item. In order to find out when the item will be registered, the browser provides the
customElements.whenDefined(...)
method. The name of the element tag is passed to it, and it returns a promise that is allowed after the element is registered.
customElements.whenDefined('my-custom-element').then(_ => { console.log('My custom element is defined'); });
For example, it may be necessary to delay the registration of an element until its child elements are declared. Such a line of conduct can be extremely useful if the project contains nested user elements. Sometimes the parent element can rely on the implementation of the child elements. In this case, you need to ensure that children are registered before the parent.
Shadow dom
As already mentioned, custom elements and Shadow DOM are complementary technologies. The first allows you to encapsulate JS logic in user elements, and the second allows you to create isolated environments for DOM fragments that are not affected by what is outside of them. If you feel that you need to better understand the concept of the Shadow DOM - take a look at one of our
previous publications .
Here is how to use the Shadow DOM for a custom item:
class MyCustomElement extends HTMLElement {
As you can see, the key here is the call to
this.attachShadow
.
Templates
In one of our
previous materials we talked a little about templates, although they are, in fact, worthy of a separate article. Here we look at a simple example of how to embed templates into custom elements when they are created. So, using the
<template>
, you can describe the DOM fragment, which will be processed by the parser, but will not be displayed on the page:
<template id="my-custom-element-template"> <div class="my-custom-element"> <input type="text" class="email" /> <button class="submit"></button> </div> </template>
Here's how to apply a template in a custom item:
let myCustomElementTemplate = document.querySelector('#my-custom-element-template'); class MyCustomElement extends HTMLElement {
As you can see, there is a combination of a custom element, a Shadow DOM, and templates. This allowed us to create an element isolated in its own space, in which the HTML structure is separated from the JS logic.
Stylization
So far we have only talked about JavaScript and HTML, bypassing CSS. Therefore, now we will touch on the theme of styles. Obviously, we need some way of styling custom elements. Styles can be added inside the Shadow DOM, but then the question arises of how to stylize such elements from the outside, for example, if the one who created them is not used. The answer to this question is quite simple - custom elements are stylized in the same way as embedded ones.
my-custom-element { border-radius: 5px; width: 30%; height: 50%;
Notice that external styles have a higher priority than styles declared inside the element, overriding them.
You may have seen how, at the time the page was displayed on the screen, at some point it is possible to observe non-stylized content on it (this is what is called FOUC - Flash Of Unstyled Content). This phenomenon can be avoided by setting styles for unregistered components, and using certain visual effects during their registration. For this you can use the selector
:defined
. You can do this, for example, as follows:
my-button:not(:defined) { height: 20px; width: 50px; opacity: 0; }
Unknown elements and undefined user elements
The HTML specification is very flexible; it allows you to declare any tags that a developer needs. And, if the tag is not recognized by the browser, it will be processed by the parser as
HTMLUnknownElement
:
var element = document.createElement('thisElementIsUnknown'); if (element instanceof HTMLUnknownElement) { console.log('The selected element is unknown'); }
However, when working with custom elements, this scheme does not apply. Remember, we talked about the rules for naming such elements? , ,
HTMLElement
.
var element = document.createElement('this-element-is-undefined'); if (element instanceof HTMLElement) { console.log('The selected element is undefined but not unknown'); }
HTMLElement
HTMLUnknownElement
, , , , - . , , , .
div
. .
Chrome 36+. API Custom Components v0, , , , . API, , —
. API Custom Elements v1 Chrome 54+ Safari 10.1+ ( ). Mozilla v50, , . , Microsoft Edge API. , , webkit. , , , — IE 11.
, , ,
customElements
window
:
const supportsCustomElements = 'customElements' in window; if (supportsCustomElements) {
:
function loadScript(src) { return new Promise(function(resolve, reject) { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); }
Results
, :
- HTML- JavaScript-, , CSS-.
- HTML- ( , ).
- . , — JavaScript, HTML, CSS, , , .
- - (Shadow DOM, , , ).
- , .
- , .
,
Custom Elements v1 , , , , , .
Dear readers! ?
