⬆️ ⬇️

How JS works: custom elements

[We advise you to read] Other 19 parts of the cycle
Part 1: Overview of the engine, execution time mechanisms, call stack

Part 2: About the V8 internals and code optimization

Part 3: Memory management, four types of memory leaks and dealing with them

Part 4: Event loop, asynchrony, and five ways to improve code with async / await

Part 5: WebSocket and HTTP / 2 + SSE. What to choose?

Part 6: Features and Scope of WebAssembly

Part 7: Web Workers and Five Use Cases

Part 8: Service Workers

Part 9: Web push notifications

Part 10: Tracking DOM Changes with MutationObserver

Part 11: The engines of rendering web pages and tips to optimize their performance

Part 12: Browser networking subsystem, optimizing its performance and security

Part 12: Browser networking subsystem, optimizing its performance and security

Part 13: Animation with CSS and JavaScript

Part 14: How JS works: abstract syntax trees, parsing and its optimization

Part 15: How JS Works: Classes and Inheritance, Babil and TypeScript Transformation

Part 16: How JS Works: Storage Systems

Part 17: How JS Works: Shadow DOM Technology and Web Components

Part 18: How JS: WebRTC and P2P Communication Mechanisms Work

Part 19: How JS Works: Custom Elements


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 the global customElements object, which has several methods:





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();   // … } // … } 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 { 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:





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 { // ... 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.





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 { // ... 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 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 { // ... } customElements.define('my-awesome-button', MyAwesomeButton);</cosourcede>      ,  , ,    <code>customElements.define(...)</code>,    <code>extends</code>   ,      .     ,        ,        DOM-.   ,          ,      ,       . <source>class MyButton extends HTMLButtonElement { // ... } 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 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 { // ... constructor() {   super();   let shadowRoot = this.attachShadow({mode: 'open'});   let elementContent = document.createElement('div');   shadowRoot.appendChild(elementContent); } // ... }); 


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 { // ... 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? , , 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) { // API Custom Elements   } 


:



 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 (supportsCustomElements) { //    ,    . } else { loadScript('path/to/custom-elements.min.js').then(_ => {   //   ,     . }); } 


Results



, :





, Custom Elements v1 , , , , , .



Dear readers! ?



Source: https://habr.com/ru/post/419831/



All Articles