We can create custom HTML elements, described by our class, with its own methods and properties, events and so on.
Once a custom element is defined, we can use it on par with built-in HTML elements.
That’s great, as HTML dictionary is rich, but not infinite. There are no , , … Just think of any other tag we might need.
We can define them with a special class, and then use as if they were always a part of HTML.
There are two kinds of custom elements:
1. Autonomous custom elements – “all-new” elements, extending the abstract HTMLElement class.
2. Customized built-in elements – extending built-in elements, like a customized button, based on HTMLButtonElement etc.
First we’ll cover autonomous elements, and then move to customized built-in ones.
To create a custom element, we need to tell the browser several details about it: how to show it, what to do when the element is added or removed to page, etc.
That’s done by making a class with special methods. That’s easy, as there are only few methods, and all of them are optional.
Here’s a sketch with the full list:
After that, we need to register the element:
Now for any HTML elements with tag , an instance of MyElement is created, and the aforementioned methods are called. We also can document.createElement(‘my-element’) in JavaScript.
Example: “time-formatted”
For example, there already exists
Observing attributes
In the current implementation of , after the element is rendered, further attribute changes don’t have any effect. That’s strange for an HTML element. Usually, when we change an attribute, like a.href, we expect the change to be immediately visible. So let’s fix this.
We can observe attributes by providing their list in observedAttributes() static getter. For such attributes, attributeChangedCallback is called when they are modified. It doesn’t trigger for other, unlisted attributes (that’s for performance reasons).
Here’s a new , that auto-updates when attributes change:
1. The rendering logic is moved to render() helper method.
2. We call it once when the element is inserted into page.
3. For a change of an attribute, listed in observedAttributes(), attributeChangedCallback triggers.
4. …and re-renders the element.
5. At the end, we can easily make a live timer.
Rendering order
When HTML parser builds the DOM, elements are processed one after another, parents before children. E.g. if we have , then element is created and connected to DOM first, and then .
That leads to important consequences for custom elements.
For example, if a custom element tries to access innerHTML in connectedCallback, it gets nothing:
If you run it, the alert is empty.
That’s exactly because there are no children on that stage, the DOM is unfinished. HTML parser connected the custom element , and is going to proceed to its children, but just didn’t yet.
If we’d like to pass information to custom element, we can use attributes. They are available immediately.
Or, if we really need the children, we can defer access to them with zero-delay setTimeout.
This works:
Now the alert in line (*) shows “John”, as we run it asynchronously, after the HTML parsing is complete. We can process children if needed and finish the initialization.
On the other hand, this solution is also not perfect. If nested custom elements also use setTimeout to initialize themselves, then they queue up: the outer setTimeout triggers first, and then the inner one.
So the outer element finishes the initialization before the inner one.
Let’s demonstrate that on example:
Output order:
1. outer connected.
2. inner connected.
3. outer initialized.
4. inner initialized.
We can clearly see that the outer element finishes initialization (3) before the inner one (4).
There’s no built-in callback that triggers after nested elements are ready. If needed, we can implement such thing on our own. For instance, inner elements can dispatch events like initialized, and outer ones can listen and react on them.
Customized built-in elements
New elements that we create, such as , don’t have any associated semantics. They are unknown to search engines, and accessibility devices can’t handle them.
But such things can be important. E.g, a search engine would be interested to know that we actually show a time. And if we’re making a special kind of button, why not reuse the existing
class HelloButton extends HTMLButtonElement { / custom element methods / }
Provide the third argument to customElements.define, that specifies the tag:
There may be different tags that share the same DOM-class, that’s why specifying extends is needed.
3. At the end, to use our custom element, insert a regular
<button is="hello-button">...</button>
Here’s a full example:
Our new button extends the built-in one. So it keeps the same styles and standard features like disabled attribute.
class MyElement extends HTMLElement {
constructor() {
super();
// element created
}
connectedCallback() {
// browser calls this method when the element is added to the document
// (can be called many times if an element is repeatedly added/removed)
}
disconnectedCallback() {
// browser calls this method when the element is removed from the document
// (can be called many times if an element is repeatedly added/removed)
}
static get observedAttributes() {
return [/* array of attribute names to monitor for changes */];
}
attributeChangedCallback(name, oldValue, newValue) {
// called when one of attributes listed above is modified
}
adoptedCallback() {
// called when the element is moved to a new document
// (happens in document.adoptNode, very rarely used)
}
// there can be other element methods and properties
}
Example:
Follow the lesson from Microsoft Web-Dev-For-Beginners course