How JS works: custom elements

Original author: Lachezar Nickolov
  • Transfer
[We advise you to read] Other 19 parts of the cycle
Часть 1: Обзор движка, механизмов времени выполнения, стека вызовов
Часть 2: О внутреннем устройстве V8 и оптимизации кода
Часть 3: Управление памятью, четыре вида утечек памяти и борьба с ними
Часть 4: Цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
Часть 5: WebSocket и HTTP/2+SSE. Что выбрать?
Часть 6: Особенности и сфера применения WebAssembly
Часть 7: Веб-воркеры и пять сценариев их использования
Часть 8: Сервис-воркеры
Часть 9: Веб push-уведомления
Часть 10: Отслеживание изменений в DOM с помощью MutationObserver
Часть 11: Движки рендеринга веб-страниц и советы по оптимизации их производительности
Часть 12: Сетевая подсистема браузеров, оптимизация её производительности и безопасности
Часть 12: Сетевая подсистема браузеров, оптимизация её производительности и безопасности
Часть 13: Анимация средствами CSS и JavaScript
Часть 14: Как работает JS: абстрактные синтаксические деревья, парсинг и его оптимизация
Часть 15: Как работает JS: классы и наследование, транспиляция в Babel и TypeScript
Часть 16: Как работает JS: системы хранения данных
Часть 17: Как работает JS: технология Shadow DOM и веб-компоненты
Часть 18: Как работает JS: WebRTC и механизмы P2P-коммуникаций
Часть 19: Как работает JS: пользовательские элементы

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.

image


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 a global object customElementsthat has several methods:

  • The method define(tagName, constructor, options)allows you to define (create, register) a new user element. It takes three arguments — the name of the tag for the custom element, which corresponds to the naming rules for such elements, the class declaration, and an object with parameters. Currently, only one parameter is supported - extendswhich is a string specifying the name of the inline element that is planned to be expanded. This feature is used to create special versions of standard elements.
  • The method get(tagName)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 method whenDefined(tagName)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 should extend the class HTMLElementand register this element with the selected name. Here's what it looks like:

classMyCustomElementextendsHTMLElement{
  constructor() {
    super();
    // …
  }
  // …
}
customElements.define('my-custom-element', MyCustomElement);

If you do not want to pollute the current scope, you can use an anonymous class:

customElements.define('my-custom-element', classextendsHTMLElement{
  constructor() {
    super();
    // …
  }
  // …
});

As you can see from the examples, the user element is registered using a method already familiar to you customElements.define(...).

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 elements nested into each other div. 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.


Appearance of the component

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-buttonclass="icon-undo"></toolbar-button>
    <toolbar-buttonclass="icon-redo"></toolbar-button>
    <toolbar-buttonclass="icon-print"></toolbar-button>
    <toolbar-toggle-buttonclass="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 - easier than the one in which there are many nested tags div.

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:

classMyCustomElementextendsHTMLElement{
  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 -). Thanks to this, the HTML parser can 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 user element - it is >my-custom-element<, and names >myCustomElement<and <my_custom_element>are unsuitable.
  • It is forbidden to register the same tag more than once. Attempting to do this will result in the browser issuing an error DOMException. Custom members cannot be overridden.
  • Custom tags cannot be self-closing. HTML-parser only supports a limited set of standard self-closing tags (e.g. - <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 a keyword in your ad thisto connect event listeners, access properties, child nodes, and so on.

classMyCustomElementextendsHTMLElement{
  // ...
  constructor() {
    super();
    this.addEventListener('mouseover', _ => {
      console.log('I have been hovered');
    });
  }
  // ...
}

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 method constructoris called once, when creating or “updating” an element (we’ll talk about this below). 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 method connectedCallbackis invoked every 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 method disconnectedCallbackis called when the 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 method attributeChangedCallbackis called when an element attribute is added, deleted, updated, or replaced. In addition, it is called when the element is created by the parser. However, note that this method applies only to the attributes that are listed in the property observedAttributes.
  • The method adoptedCallbackis called when the method document.adoptNode(...)used to move the node to another document is called.

Please note that all the above methods are synchronous. For example, a method connectedCallbackis called immediately after an element is added to the DOM, and the rest of the program waits for the end of the execution of this method.

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 it is characteristic of a property id. For example, perform the following operation:

myDiv.id = 'new-id';

Relevant changes will also affect DOM:

<divid="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.

classMyCustomElementextendsHTMLElement{
  // ...
  get myProperty() {
    returnthis.hasAttribute('my-property');
  }
  set myProperty(newValue) {
    if (newValue) {
      this.setAttribute('my-property', newValue);
    } else {
      this.removeAttribute('my-property');
    }
  }
  // ...
}

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 using the keyword extendswhen declaring a class:

classMyAwesomeButtonextendsMyButton{
  // ...
}
customElements.define('my-awesome-button', MyAwesomeButton);</cosourcede>
В случае со стандартными элементами нужно, кроме того, использовать, при вызове метода <code>customElements.define(...)</code>, объект со свойством <code>extends</code> и со значением, представляющим собой имя тега расширяемого элемента. Это сообщает браузеру о том, какой именно элемент является основой нового пользовательского элемента, так как множество встроенных элементов имеют одинаковые DOM-интерфейсы. Без указания того, какой именно элемент используется в качестве основы для пользовательского элемента, браузер не будет знать о том, на какой именно функциональности базируется новый элемент.
<source>classMyButtonextendsHTMLButtonElement{
  // ...
}
customElements.define('my-button', MyButton, {extends: 'button'});

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 method is customElements.define(...)used to register custom items. However, registration can not be called the action that needs to be performed in the first place. 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 know when the item will be registered, the browser provides a method customElements.whenDefined(...). The name of the element tag is passed to it, and it returns a promise, which 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:

classMyCustomElementextendsHTMLElement{
  // ...
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: 'open'});
    let elementContent = document.createElement('div');
    shadowRoot.appendChild(elementContent);
  }
  // ...
});

As you can see, the challenge here is the challenge 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 tag <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');
classMyCustomElementextendsHTMLElement{
  // ...
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(myCustomElementTemplate.content.cloneNode(true));
  }
  // ...
});

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? When the browser encounters a similar element having a well-formed name, it will be processed by the parser as HTMLElementwill be presented by the browser as an undefined user element.

var element = document.createElement('this-element-is-undefined');
if (element instanceof HTMLElement) {
  console.log('The selected element is undefined but not unknown');
}

Although outwardly HTMLElementand HTMLUnknownElementmay not differ, some of their features, still, it is worth remembering, since they are handled differently in the parser. An element that has a name that conforms to the rules for naming user elements is expected to have its implementation. Before its registration, such an element is considered as an empty element div. However, an undefined user element does not implement any methods or properties of embedded elements.

Browser Support


Support for the first version of custom elements first appeared in Chrome 36+. This was the so-called API Custom Components v0, which is now considered obsolete, and, although it is still available, it is not recommended to use it. If this API is for you, nevertheless, it is interesting - take a look at this material. The Custom Elements v1 API is available in Chrome 54+ and Safari 10.1+ (although not completely). In Mozilla, this feature is present since v50, but by default it is disabled, it must be explicitly enabled. It is known that Microsoft Edge is working on the implementation of this API. I must say that fully custom components are available only in webkit based browsers. However, as already mentioned, there are polyfills that allow you to work with them in any browsers - even in IE 11.

Checking the ability to work with custom elements


In order to find out if the browser supports working with custom elements, you can perform a simple check for the existence of a property customElements
in an object window:

const supportsCustomElements = 'customElements'inwindow;
if (supportsCustomElements) {
  // API Custom Elements можно пользоваться
}

When using a polyfill, it looks like this:

functionloadScript(src) {
  returnnewPromise(function(resolve, reject) {
    const script = document.createElement('script');
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}
// Если полифилл нужен - выполняем его ленивую загрузку.if (supportsCustomElements) {
  // Браузер поддерживает пользовательские элементы, с ними можно работать.
} else {
  loadScript('path/to/custom-elements.min.js').then(_ => {
    // Соответствующий полифилл загружен, с пользовательскими элементами можно работать.
  });
}

Results


In this material, we talked about custom elements that give the developer the following features:

  • They allow you to bind JavaScript code describing its behavior to an HTML element and associate its CSS styling with it.
  • They make it possible to extend existing HTML elements (both embedded and custom).
  • To work with custom elements do not need additional libraries or frameworks. All you need is the usual JavaScript, HTML, CSS, and, if the browser does not support custom elements, the corresponding polyfill.
  • Custom elements are created based on their use with other features of the web components (Shadow DOM, templates, slots, and so on).
  • Support for custom elements is tightly integrated into the browser developer tools that implement this standard.
  • When using custom elements, you can take advantage of the features already available in other elements.

It should be noted that the support of the standard Custom Elements v1 by browsers is still at an average level, however, everything indicates that, in the foreseeable future, the situation may well change for the better.

Dear readers! Do you plan to use custom elements in your projects?


Also popular now: