How JS works: custom elements

[Советуем почитать]The previous 18 parts of the cycle [/b]
Part 1: Engine overview, runtime mechanisms, call stack
 
Part 2: About the internal device V8 and optimization of the code
 
Part 3: Memory management, four types of memory leaks and fighting them
 
Part 4: The event loop, asynchrony, and five ways to improve the code with async /await
 
Part 5: WebSocket and HTTP /2 + SSE. What to choose?
 
Part 6: Features and scope of WebAssembly
 
Part 7: Web-riches and five scenarios for their use
 
Part 8: Service-riches
 
Part 9: Web push notifications
 
Part 10: Tracking changes in the DOM with MutationObserver
 
Part 11: Web page rendering engines and tips for optimizing their performance
 
Part 12: The network subsystem of browsers, optimization of its performance and security
 
Part 12: The network subsystem of browsers, optimization of its performance and security
 
Part 13: Animation using CSS and jаvascript
 
Part 14: How JS works: abstract syntax trees, parsing and its optimization.
 
Part 15: How JS Works: Classes and Inheritance, Transfers in Babel and TypeScript
 
Part 16: How JS works: Storage systems
 
Part 17: How JS works: Shadow DOM technology and Web components
 
Part 18: How JS works: WebRTC and the mechanisms of P2P communications
 
We present to your attention the translation of 19 articles from a series of materials of the company SessionStack , dedicated to the peculiarities of various mechanisms of the jаvascript ecosystem. Today we will talk about the standard Custom Elements - about the so-called "user elements". We will talk about what tasks they can solve, and how to create and use them.
 
 
How JS works: custom elements

 
Shadow DOM and about some other technologies that are part of a larger phenomenon - web components. Web components are designed to give developers the ability to extend the standard HTML capabilities by creating compact, modular and reusable elements. This is a relatively new standard of the W3C, which has already attracted the attention of manufacturers of all leading browsers. It can be found in production, although, of course, while its work is provided by polyphiles (we'll talk about them later).
 
 
As you may already know, browsers give us some of the most important tools for developing websites and web applications. It's about HTML, CSS and jаvascript. HTML is used for structuring web pages, thanks to CSS they are given a nice appearance, and jаvascript is responsible for interactive features. However, before the advent of Web components, it was not so easy to link the actions implemented by jаvascript to the HTML structure.
 
 
Strictly speaking, here we will consider the basis of Web components - custom elements (Custom Elements). If we talk about them in a few words, then the API, designed to work with them, allows the programmer to create their own HTML-elements with the built-in jаvascript-logic and styles described by CSS. Many confuse user elements with Shadow DOM technology. However, these are two completely different things that, in fact, supplement each other, but are not interchangeable.
 
 
Some frameworks (such as Angular or React) try to solve the same problem that user elements solve by introducing their own concepts. User elements can be compared to Angular directives or React components. However, custom elements are a standard feature of the browser, you do not need anything other than normal jаvascript, HTML, and CSS to work with them. Of course, this does not allow us to say that they are a replacement for conventional JS-frameworks. Modern frameworks give us much more than just the ability to mimic the behavior of user elements. As a result, we can say that both frameworks and user elements are technologies that can be used together to solve web development tasks.
 
 


API


 
Before we continue, let's see what opportunities the API gives us for working with custom elements. Namely, we are talking about the global object customElements , which has several methods:
 
 
  •  
  • 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 user element, corresponding to the naming rules for such elements, the class declaration and the object with the parameters. At the moment only one parameter is supported: extends , which is a string specifying the name of the built-in element that you plan to extend. This feature is used to create special versions of standard elements.  
  • Method get (tagName) returns the constructor of the user element, provided that this element is already defined, otherwise it returns undefined . It takes one argument, the name of the custom element tag.  
  • Method whenDefined (tagName) returns an indentation that is resolved after the user element is created. If the element is already defined, this promise is resolved immediately. Promise is rejected if the tag name passed to it is not a valid name for the user element tag. This method takes the name of the custom element tag.  

 

Creating custom elements


 
Creating custom items is easy. To do this, you need to do two things: create a class declaration for the element that should extend the class HTMLElement and register this item under the selected name. Here's how it looks:
 
 
    class MyCustomElement extends HTMLElement {
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', class extends HTMLElement {
.structor () {
super ();
//
}
.
//
});

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

Problems that are solved by custom elements


 
Let's talk about the problems that users can solve. One of them is the improvement of the code structure and the elimination of what is called the "div soup" soup. This phenomenon is a very common structure in modern web applications code structure, in which there are many nested elements div . Here's how it might look:
 
 
    












 
Such HTML-code is used for quite reasonable reasons - it describes the page device and ensures its correct output to the screen. However, this worsens the readability of the HTML-code and complicates its support.
 
 
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 describing such things, this component will correspond to the following code:
 
 
    








































 
And now imagine that we could, instead of this code, use this description of the component:
 
 

 
I'm sure everyone will agree that the second code fragment looks much better. Such code is easier to read, easier to maintain, it is understandable to both the developer and the browser. It all boils down to the fact that it is simpler than the one in which there are many nested tags div .
 
 
The next problem that can be solved with the help of custom elements is the reuse of the code. The code that developers write should be not only working, but also supported. Reusing the code, as opposed to constantly writing the same designs, 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:
 
 
    



 
If there is a constant 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 to this code, which should be reflected everywhere where it is used. This means that we need to find all the places where this fragment is used, after which we make the same changes everywhere. This is long, hard and fraught with mistakes.
 
 
It would be much better if we could, where this element is needed, just write the following:
 
 

 
However, modern web applications are much more than static HTML code. They are interactive. The source of their interactivity is jаvascript. Usually, to provide such opportunities, create certain elements, then connect to them the listeners of events, which allows them to respond to the user's actions. For example, they can react to clicks, hanging the mouse over them, dragging them across the screen, and so on. Here's how to connect the listener to the event that occurs when you click on it with the mouse:
 
 
    var myDiv = document.querySelector ('. my-custom-element');
myDiv.addEventListener ('click', _ => {
myDiv.innerHTML = ' I have been clicked ';
});

 
And here is the HTML-code of this element:
 
 
    

I have not been clicked yet.

 
By using the API to work with custom elements, all this logic can be included in the element itself. For comparison, Here is the code for declaring a custom element that includes an event handler:
 
 
    class MyCustomElement extends HTMLElement {
constructor () {
super ();
var self = this;
self.addEventListener ('click', _ => {
self.innerHTML = ' I have been clicked ';
});
}
}
customElements.define ('my-custom-element', MyCustomElement);

 
And here is how it looks in the HTML-code of the page:
 
 
    
I have not been clicked yet

 
At first glance it may seem that more JS-code lines are required to create a custom element. However, in real applications, it rarely happens that such elements are created only in order to use them only once. Another common phenomenon in modern web applications is the fact 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, by means of jаvascript, and situations when they are described in the original HTML page structure. Thanks to the use of custom elements, work in these two situations is simplified.
 
 
As a result, if we summarize the results of this section, we can say that the user elements make the code more understandable, simplify its support, help to break it into small modules, including all the necessary functionality and suitable for reuse.
 
 
Now that we have discussed the general issues of working with custom elements, let's talk about their features.
 
 

Requirements


 
Before you begin to develop your own custom elements, you should know about some rules that you need to follow when creating them. Here they are:
 
 
  •  
  • The component name must include a hyphen (character - ). Thanks to this, the HTML parser can distinguish between built-in and custom elements. In addition, this approach ensures the absence of collisions of names with built-in elements (and with those that are now, and with those that will appear in the future). For example, the actual name of the user element is > my-custom-element < , and the names are > myCustomElement < and are unsuitable.  
  • It is forbidden to register the same tag more than once. Trying to do this will result in the browser issuing the error DOMException . Custom elements can not be overridden.  
  • Custom tags can not be self-closing. The HTML parser supports only a limited set of standard self-closing tags (for example - . . , . . , .
    ).  

 

Features


 
Let's talk about what you can do with custom elements. If you answer this question in a few words, it turns out that you can do a lot of interesting things with them.
 
 
One of the most notable features of user-defined elements is that the declaration of an element class refers to the DOM element itself. This means that you can use the keyword in your ad. this to connect event listeners, to access properties, to child nodes, and so on.
 
 
    class MyCustomElement extends HTMLElement {
//
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, doing this is not recommended, as this can lead to unexpected behavior of the elements. If you imagine that you are using elements that are designed by someone else, then you probably will 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 the life cycle of an element.
 
 
  •  
  • Method constructor It is called once, when an element is created or "upgraded" (this we will discuss below). Most often it is used to initialize the state of an element, to connect listeners to events, create a Shadow DOM, and so on. Do not forget that in the designer you should always call super () .  
  • Method connectedCallback It is called every time an item is added to the DOM. It can be used (and this is how it is recommended to use it) in order to postpone the execution of any actions until the element appears on the page (for example, you can postpone the loading of any data).  
  • Method disconnectedCallback Called when an item is removed from the DOM. It is usually used to free 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.  
  • Method attributeChangedCallback It is called when an element's attribute is added, deleted, updated, or replaced. In addition, it is called when the item is created by a parser. However, note that this method only applies to attributes that are listed in property observedAttributes .  
  • Method adoptedCallback is called when the method is called. document.adoptNode () used to move a node to another document.  

 
Please note that all the above methods are synchronous. For example, the method connectedCallback It is called immediately after the element is added to the DOM, and the rest of the program waits for the completion of this method.
 
 

Reflection of the properties


 
Embedded HTML elements have one very convenient feature: property reflection. Due to this mechanism, the values ​​of some properties are directly reflected in the DOM as attributes. For example, this is characteristic of the property id . For example, let's perform this operation:
 
 
    myDiv.id = 'new-id';    

 
Corresponding changes will affect the DOM:
 
 
    


 
This mechanism also acts in the opposite direction. It is very useful, because it allows you to declaratively configure the elements.
 
 
Custom elements do not have such a built-in capability, but you can implement it yourself. In order for some properties of user elements to behave in a similar way, you can customize their getters and setters.
 
 
    class MyCustomElement extends HTMLElement {
//
get myProperty () {
return this.hasAttribute ('my-property');
}
set myProperty (newValue) {
if (newValue) {
this.setAttribute ('my-property', newValue);
} else {
this.removeAttribute ('my-property');
}
}
//
}

 

Expansion of existing elements


 
The API of user-defined elements allows not only to create new HTML-elements, but also to extend existing ones. Moreover, we are talking about the standard elements, and about the user. This is done using the keyword extends when declaring a class:
 
 
    class MyAwesomeButton extends MyButton {
//
}
customElements.define ('my-awesome-button', MyAwesomeButton);
In the case of standard elements, it is also necessary to use, when calling the method customElements.define () , an object with the property extends and with a value that is the name of the expandable element tag. This tells the browser which element is the basis of the new user element, since many built-in elements have the same DOM interfaces. Without specifying which element is used as the basis for the user element, the browser will not know which functionality the new element is based on.
class MyButton extends HTMLButtonElement {
//
}
customElements.define ('my-button', MyButton, {extends: 'button'});

 
Extended standard elements are also called "customized built-in elements".
 
 
It is recommended that it is always a rule to expand existing elements, and to do it progressively. This will allow you to save in the new elements the features that were implemented in the previously created elements (that is, properties, attributes, functions).
 
 
Please note that custom built-in elements are now supported only in Chrome 67+. This will appear in other browsers, however, it is known that the developers of Safari decided not to implement this opportunity.
 
 

Updating the elements


 
As already mentioned, the method customElements.define () is used to register user items. However, registration can not be called the action that must be performed first. Registration of the user element can be postponed for a while, and this time can come even when the element is already added to the DOM. This process is called an upgrade of the element (upgrade). In order to find out when an element is registered, the browser provides the method customElements.whenDefined () . It is passed the name of the element tag, and it returns the permissive that is allowed after the element is registered.
 
 
    customElements.whenDefined ('my-custom-element'). then (_ => {
console.log ('My custom element is defined');
});

 
For example, you might need to defer the registration of an element until its child elements are declared. Such a line of behavior can be extremely useful in the event that the project has nested user elements. Sometimes a parent element can rely on the implementation of child elements. In this case, you need to ensure that the child elements are registered before the parent.
 
 

Shadow DOM


 
As already mentioned, custom elements and Shadow DOM are mutually complementary technologies. The first allows JS logic to be encapsulated in user elements, and the second one 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 Shadow DOM - take a look at one of our previous publications .
 
 
Here's how to use the Shadow DOM for a custom element:
 
 
    class MyCustomElement extends HTMLElement {
//
constructor () {
super ();
let shadowRoot = this.attachShadow ({mode: 'open'});
let elementContent = document.createElement ('div');
shadowRoot.appendChild (elementContent);
}
//
});

 
As you can see, the key role here is played by the call of this.attachShadow .
 
 

Templates


 
In one of our previous materials we talked a little about templates, although they, in fact, are worthy of a separate article. Here we'll look at a simple example of how to embed templates in custom elements when they are created. So, using the tag , you can describe the DOM fragment that will be parsed, but the page will not be displayed:
 
 
    




 
Here's how to apply a template to a custom element:
 
 
    let myCustomElementTemplate = document.querySelector ('# my-custom-element-template');
class MyCustomElement extends HTMLElement {
//
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 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 jаvascript and HTML, bypassing the attention of CSS. So 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 as to how to stylize such elements from the outside, for example - if you use them not the one who created them. The answer to this question is quite simple - styling user elements just like the built-in ones.
 
 
    my-custom-element {
border-radius: 5px;
width: 30%;
height: 50%;
//
}

 
Note that external styles have a higher priority than styles declared within an element, redefining them.
 
 
Perhaps you have seen how, at the time the page is displayed, at some point you can see unlisted content on it (this is what is called FOUC - Flash Of Unstyled Content). You can avoid this phenomenon by asking styles for unregistered components, and using some visual effects when registering them. To do this, you can use the selector : defined . To do this, for example, you can do so:
 
 
    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 developer-specific tags. And, if the tag is not recognized by the browser, it will be parsed 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, such a scheme does not apply. Remember, we talked about the rules for naming such elements? When a browser encounters a similar element with a properly formed name, it will be parsed by the parser as HTMLElement and will be represented 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 HTMLElement and HTMLUnknownElement may not differ, some of their features, yet, it is worth remembering, since they are treated differently in a parser. From an element that has a name that conforms to the naming rules for user elements, its implementation is expected. Prior to its registration, such an element is treated as an empty element div . However, an undefined user element does not implement any methods or properties of built-in elements.
 
 

Browser support


 
Support for the first version of custom elements first appeared in Chrome 36+. This was the so-called API Components Components v? which is now considered obsolete, and although it is still available, it is not recommended to use it. If you need this API, it's interesting, take a look at this is material. API Custom Elements v1 is available in Chrome 54+ and Safari 10.1+ (although not completely). In Mozilla, this feature is present since v5? but by default it is disabled, you need to explicitly enable it. 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 browsers based on webkit. However, as already mentioned, there are polyphiles that allow you to work with them in any browser - even in IE 11.
 
 

Verification of the possibility of working with custom elements


 
In order to find out if the browser supports the work with custom elements, you can perform a simple test for the existence of the property. customElements
 
in the object window :
 
 
    const supportsCustomElements = 'customElements' in window;
if (supportsCustomElements) {
//API Custom Elements can use
}

 
When using a polyphile it looks like this:
 
 
    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);
});
}
//If polifill is needed - we perform its lazy loading.
if (supportsCustomElements) {
//The browser supports custom elements, you can work with them.
} else {
loadScript ('path /to /custom-elements.min.js'). then (_ => {
//The corresponding polyphyle is loaded, you can work with custom elements.)
});
}

 


Results of


 
In this article we talked about user elements that give the developer the following possibilities:
 
 
 
They allow you to anchor the HTML-element to the HTML-code, describing its behavior, and to associate with it its CSS-stylization.
 
They allow you to extend existing HTML elements (both built-in and custom).
 
To work with custom elements, you do not need additional libraries or frameworks. All you need is ordinary jаvascript, HTML, CSS, and if the browser does not support custom elements, the corresponding polyphile.
 
Custom elements are designed to be used together with other features of the Web components (Shadow DOM, templates, slots, and so on).
 
Support for custom elements is tightly integrated into browser development tools that implement this standard.
 
When using custom elements, you can take advantage of the capabilities already available from other elements.
 
 
It should be noted that Support the standard Custom Elements v1 browsers are still at the middle level, however, all points to the fact 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?
 
 
+ 0 -

Add comment