If you missed the previous chapters, you can find them here:
- An overview of the engine, the runtime, and the call stack
- Inside Google’s V8 engine + 5 tips on how to write optimized code
- Memory management + how to handle 4 common memory leaks
- The event loop and the rise of Async programming + 5 ways to better coding with async/await
- Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
- The building blocks of Web Workers + 5 cases when you should use them
- Service Workers, their life-cycle, and use cases
- The mechanics of Web Push Notifications
- Tracking changes in the DOM using MutationObserver
- The rendering engine and tips to optimize its performance
- Inside the Networking Layer + How to Optimize Its Performance and Security
- Under the hood of CSS and JS animations + how to optimize their performance
- Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
- The internals of classes and inheritance + transpiling in Babel and TypeScript
- Storage engines + how to choose the proper storage API
- The internals of Shadow DOM + how to build self-contained components
- WebRTC and the mechanics of peer to peer connectivity
In one of our previous posts we discussed the Shadow DOM API and a few other concepts which are all parts of a bigger picture — web components. The whole idea behind the web components standard is to be able to extend the built-in capabilities of HTML by creating small, modular, and reusable elements. It’s a relatively new W3C standard that has already been approved by all major browsers and can be seen in production environments… of course with the help of a polyfill library (which we’re going to talk about later in the post).
Before we dive in, let’s take a quick look over what the API actually looks like. The
customElements global object gives you a few methods:
define(tagName, constructor, options)— Defines a new custom element. Takes three arguments: A valid tag name for a custom element, class definition for the custom element, and an options object. Only one option is supported currently:
extendswhich is a string specifying the name of a built-in element to extend. Used to create a customized built-in elements.
get(tagName)— Returns the constructor of a custom element if the element is defined and returns undefined otherwise. Takes a single argument: A valid tag name for a custom element.
whenDefined(tagName)— Returns a promise which is resolved once a custom element is defined. If the element is already defined, it will be resolved immediately. The promise is rejected if the tag name is not a valid custom element name. Takes a single argument: A valid tag name for a custom element
How to create a custom element
Creating a custom element is actually a piece of cake. You need to do two things: Create a class definition for the element which should extend the
HTMLElement class and register that element under a name of your choice.
Or if you want, you can use anonymous class in case you don’t want to clutter the current scope
As you can see from the examples, custom elements are registered by using the
What issues are custom elements solving
So what’s the problem actually. Div soups are part of it. What is a div soup you might ask — it’s a very common structure in modern web apps where you have multiple nested div elements (div inside a div inside a div and so on).
This kind of structure is used since it makes the browser render the page as it should. However, it makes the HTML unreadable and very hard to maintain.
So for example we might have a component which is supposed to look like this
And traditionally the HTML might look like the following.
But imagine if we could make it look like this instead
The second example is much better, if you ask me. It’s more maintainable, readable, and it makes sense both for the browser and the developer. It’s just simpler.
The other issue is reusability. Our job as developers requires not only to write working code but also a maintainable one. And one thing that makes some code maintainable is being able to easily reuse a piece of code instead of writing it again and again.
I’ll give you a simple example but you’ll get the idea. Let’s say we have the following element:
If we need to use this elsewhere we’d need to write the same HTML all over again. Now imagine that we need to do a change that needs to apply to each of those elements. We’d need to find each place in the code and do the exact same change again and again. Bummer…
Wouldn’t it be better if we could just do the following
With the custom elements API all this logic can be encapsulated into the element itself. The example below does exactly the same as the one above.
So to summarize, custom elements make your code easier to understand and maintain, and splits it into small, reusable and encapsulated modules.
Now before you go on and create your first custom element, you should know that there are special rules that must be followed.
- The name must contain a dash (-) in it. This way the HTML parser can tell which elements are custom and which are built-in. It also ensures that there won’t be a name collision with built-in elements (either now or in the future when other ones are added). For example,
<my-custom-element>is a valid name while
- Registering the same tag name more than once is forbidden. This will cause the browser to throw a
DOMException. You cannot override custom elements.
- Custom elements cannot be self-closing. The HTML parser allows only a handful of built-in elements to be self-closing (e.g.
So what actually can you do with custom elements? And the answer is — many things.
One of the best features is that the class definition of the element is actually referring to the DOM element itself. This means that you can use
this directly to attach event listeners, access its properties, access child nodes, and so on.
This, of course, gives you the ability to overwrite the child nodes of an element with new content. But this is generally not recommended, since it might lead to unexpected behavior. As a user of a custom element that’s not implemented by yourself you will be surprised if your own markup inside the element is replaced by something else.
There are a few hooks that you can define for executing code at specific times of the element’s lifecycle.
The constructor is called once the element is created or upgraded (we’ll talk about this in a bit). It’s most commonly used for state initialization, attaching event listeners, creating a shadow DOM, etc. One thing to keep in mind is that you should always call
super() in the constructor.
connectedCallback method is called each time the element is added to the DOM. It can be used (it’s also recommended) to delay some work until the element is actually on the page (e.g. fetching a resource).
disconnectedCallback method is called once an element is taken out of the DOM. Usually used for freeing up resources. One thing to have in mind is that the
disconnectedCallback is never called if the user closes the tab. So be careful what you’re initializing in the first place.
This method is called once an attribute of the element has been added, removed, updated or replaced. It’s also called once the element is being created by the parser. However, note that this applies only for attributes which are whitelisted in the
addoptedCallback method is called once the
document.adoptNode(...) method is called in order to move it to a different document.
Note that all of the callbacks above are synchronous. For example, the connected callback is called immediately after the element is added to the DOM and nothing else happens in the meantime.
Built-in HTML elements provide one very handy capability: property reflection. This means that the values of some properties are directly reflected back to the DOM as an attribute. Such example is the
myDiv.id = 'new-id';
Will also update the DOM to
<div id="new-id"> ... </div>
And it applies in the opposite direction as well. This is very useful since it allows you to configure elements declaratively.
Custom elements don’t get this kind of functionality out of the box but there is a way to implement it on your own. In order to achieve the same behavior in our custom elements we can define getters and setters for the properties.
The custom elements API allows you not only to create new HTML elements but to also extend existing ones. And it works perfectly fine both for built-in elements and other custom ones. And it’s done just by extending its class definition.
Or in the case of built-in elements we need to also add a third parameter to the
customElements.define(...) function which is an object with a property
extends and a value the tag name of the element that’s being extended. This tells the browser which element exactly is being extended since many built-in elements share the same DOM interface. Without specifying which element exactly you’re extending, the browser won’t know what kind of functionality is being extended.
An extended native element is also called a customized built-in element.
What you can use as a rule of thumb is to always extend existing elements. And do this progressively. This allows you to keep all of the previous features (properties, attributes, functions).
Note that customized built-in elements are only supported by Chrome 67+ right now. It will be implemented in the other browsers as well but Safari has chosen not to implement it at all.
As mentioned above, we use the
customElements.define(...) method to register a custom element. But this doesn’t mean that it’s the first thing that you have to do. Registering a custom element can be postponed for some time in the future. Even after the element itself is added to the DOM. This process is called element upgrade. To let you know when the element is actually defined, the browser provides you the
customElements.whenDefined(...) method. You pass it the tag name of the element it returns a promise which is resolved once the element is registered.
For example, you might want to delay something until all child elements are defined. Which can be really useful if you have nested custom elements. Sometimes the parent element might rely on the implementation of its children. In this case, you need to make sure that the child elements are defined before their parent.
And to use shadow DOM for your custom element you simply need to call
We’ve briefly talked about templates in one of our previous posts and they alone deserve a post of their own. Here we’re going to give a simple example how you can incorporate templates into the creation of your custom elements. Using the
<template> you can declare a DOM fragment tag which is parsed but not rendered on the page.
Note that a style defined from the outside is with a higher priority and it will override the style defined from the element.
You know how sometimes you can actually see the page render and you see for a brief moment some flash of unstyled content (FOUC). You can avoid this by defining styles for undefined components and use some kind of transition when they become defined. To do this you can use the :defined selector.
The HTML specification is very flexible and allows declaration of whatever tag you want to. And if the tag is not recognized by the browser it will be parsed as
However, this does not apply for custom elements. Remember when we talked that there are specific naming rules for defining custom elements? The reason is that if the browser sees a valid name for a custom element it will parse it as an
HTMLElement and is considered by the browser to be an undefined custom element.
While there might not be any visual differences between the HTMLElement and HTMLUnknownElement there are other things to keep in mind. They are treated differently by the parser. An element with a valid custom element name is expected to have a custom implementation. And until that implementation is defined it’s treated just like an empty div element. While an undefined element does not implement any method or property of any built-in element.
The first version of custom elements was introduced in Chrome 36+. It was the so called custom components API v0 which is now deprecated and considered a bad practice although still available. Although, if you want to learn more about v0 you can read about it in this blog post. The custom elements API v1 is available since Chrome 54 and Safari 10.1 (although partially). Microsoft’s Edge is in its prototyping phase and Mozilla has it since v50 but it’s not available by default and needs to be enabled explicitly. At the moment only webkit browsers support it fully. However, as mentioned above, there’s a polyfill that allows you to use custom elements across all browsers. Yes, even IE 11.
To make sure that the browser supports custom elements you can do a simple check whether the
customElements property exists in the
Or in case you’re using the polyfill library:
So to summarize, the custom elements part of the web components standard gives you the following:
- Allows you to extend already existing HTML elements (both built-in and other custom ones)
- It’s built to work seamlessly with other web components features (shadow DOM, templates, slots, etc.)
- Tightly integrated with the browser’s dev tools.
- Leverage existing accessibility features.
Custom elements are not that different from what we’ve been using until now after all. It’s just another way to make things more convenient while developing web apps. So it opens up the possibility to build very complex apps at a faster pace. But the higher the complexity the higher the chance of introducing an issue that’s hard to track down and reproduce. That’s why debbuging them requires more context and a tool like SessionStack makes a difference.
SessionStack gets integrated into into web apps to collect data such as user events, network data, exceptions, debug messages, DOM changes, and so on, and to send this data to our servers.
After that, the collected data is processed in order to create a video like experience so you can see your users interacting with your product. This, alongside all of the technical information SessionStack provides, gives you the ability to reproduce issues you’ve never been able to track down before.
So in order to ensure that SessionStack will always produce pixel perfect session replay we need to keep up with the arising technologies, frameworks, and web standards.
There is a free plan if you’d like to give SessionStack a try.