Custom Elements
Custom Elements (自定义元素)
Custom HTML elements can be created by a class with its properties and methods, events, and more. (自定义HTML元素可以由类及其属性和方法、事件等创建。)
When a custom element is specified, it can be used in parallel with built-in HTML elements. (当指定自定义元素时,它可以与内置HTML元素并行使用。)
Two kinds of custom elements can be distinguished:
Autonomous: to this group are related the “all-new” elements that extend the HTMLElement class.
Customized built-in elements: these are the ones that extend built-in elements such as a customized button built on the HTMLButtonElement.
We will start at discovering the autonomous custom elements. For generating a custom element, it’s necessary to inform the browser about several details about it: how to act when the element is added or removed, how to demonstrate it, and so on.
You can do it by generating a class with specific methods. (您可以通过使用特定方法生成类来实现此目的。)
The sketch containing the full list will look like this:
class CustomElement extends HTMLElement {
constructor() {
super();
// element created
(//元素已创建)
}
connectedCallback() {
//the browser calls this method when an element is added to the document
(//当向文档添加元素时,浏览器调用此方法)
// (it can be called many times if an element is added/removed many times)
(//(如果多次添加/删除元素,则可以多次调用))
}
disconnectedCallback() {
// the browser calls this method, when the element is removed from the document
(//当从文档中删除元素时,浏览器调用此方法)
// (it can be called many times if an element is added/removed many times)
(//(如果多次添加/删除元素,则可以多次调用))
}
static get observedAttributes() {
return [ /*array of attribute names for change tracking*/ ];
}
attributeChangedCallback(name, oldValue, newValue) {
// called when one of the listed attributes is changed
(//当列出的属性之一发生更改时调用)
}
adoptedCallback() {
// called once the element is transferred to a new document
(//元素传输到新文档后调用)
//occurs in document.adoptNode, applied very seldom
(//发生在document.adoptNode中,很少应用)
}
// there may be other methods and properties of the element
}
Then, it’s required to register the element:
// tell the browser that <we-element> is served by our new class
customElements.define("custom-element", CustomElement);
For each HTML element with <custom-element> tag, a CustomElement instance is generated, and the methods above are called.
In JavaScript, it is also possible to call document.createElement(‘custom-element’). (在JavaScript中,也可以调用document.createElement (‘custom-element’)。)
It’s essential to know that the name of a custom element must contain a hyphen - (for example, custom֊element))). To be more precise let’s consider an example of creating <time-formatted> element.
In HTML, there is a <time> element, which doesn’t do formatting itself. So, the element will display the time in a language-aware format, like this:
<!DOCTYPE html>
<html>
<head>
<title>Title of the document</title>
</head>
<body>
<!-- (3) -->
<time-format datetime="2020-04-11" year="numeric" month="long" day="numeric" hour="numeric" minute="numeric" second="numeric" time-zone-name="short"></time-format>
<script>
class TimeFormat extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
}
customElements.define("time-format", TimeFormat); // (2)
</script>
</body>
</html>
May 11, 2020, 4:00:00 AM GMT+4
The class includes a single connectedCallback() method. The browser will call it once the <time-formatted> element is attached to the page. It applies the built-in Intl.DateTimeFormat, supported by browsers for demonstrating a proper-formatted time. It is required to register the new element by customElements.define(tag, class). Then, it can be used everywhere.
Upgrading Custom Elements
In case the browser faces-off the <time-formatted> elements before customElements.define. That’s not considered an error. But, the element is not revealed yet.
However, you can style undefined elements with CSS selector: not(:defined). For getting information about custom elements, the methods below are used: customElements.get(name): returning the class for a custom element with a specific name.
customElements.whenDefined(name): returning a promise, which resolves once a custom element with a specific name is defined.
Rendering
It’s important to note that rendering should take place in connectedCallback and not in the constructor. (需要注意的是,渲染应该在connectedCallback中进行,而不是在构造函数中进行。)
The connectedCallback occurs when the element is inserted into a document: not just appended but becomes a part of the page. So, a detached DOM can be built.
Observing Attributes
Observing Attributes (观察属性)
In the course of the <time-formatted> implementations, after the rendering of the element, further attribute changes will not be efficient.
As a rule, while changing an attribute such as a.href, the change is expected to be visible at once. (通常,在更改a.href等属性时,更改预计会立即可见。)
The attributes can be observed through a list in observedAttributes(). The attributeChangedCallback is called once they are changed. (可以通过observedAttributes ()中的列表观察属性。attributeChangedCallback在更改后调用。)
Let’s see a new <time-formatted> capable of auto-updating when attributes are modified:
<!DOCTYPE html>
<html>
<head>
<title>Title of the document</title>
</head>
<body>
<time-format id="elem" hour="numeric" minute="numeric" second="numeric"></time-format>
<script>
class TimeFormatted extends HTMLElement {
render() { // ( 1 )
let dateTime = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
y: this.getAttribute('year') || undefined,
m: this.getAttribute('month') || undefined,
d: this.getAttribute('day') || undefined,
h: this.getAttribute('hour') || undefined,
m: this.getAttribute('minute') || undefined,
s: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(dateTime);
}
connectedCallback() { // (2)
if(!this.rendered) {
this.render();
this.rendered = true;
}
}
static get observedAttributes() { // (3)
return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
}
attributeChangedCallback(name, oldValue, newValue) { // (4)
this.render();
}
}
customElements.define("time-format", TimeFormatted);
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
customElements.define("time-formatted", TimeFormatted); // (2)
</script>
</body>
</html>
So, let’s clarify what happens here:
The logic of the rendering is transferred to the render() helper method. It is called once when the element is put into the page. For an attribute modification, listed inside observedAttributes(), attributeChangedCallback occurs. Then, the element is re-rendered. And, a live-timer is created at the end. (渲染的逻辑被传递到render () helper方法。 元素放入页面时调用一次。 对于在observedAttributes ()中列出的属性修改,会发生attributeChangedCallback。 然后,重新渲染元素。 最后会创建一个实时计时器。)
Rendering Order
Rendering Order (渲染顺序)
Once HTML parser develops the DOM, the elements are progressed one after another. For example, when there is <outer><inner></inner></outer>, then the <outer> element is generated and linked to DOM first, and <inner>, afterward. It brings crucial consequences for custom elements.
When a custom element attempts to access innerHTML in connectedCallback, it will not get anything:
<!DOCTYPE html>
<html>
<head>
<title>Title of the document</title>
</head>
<body>
<script>
customElements.define('site-info', class extends HTMLElement {
connectedCallback() {
alert(this.innerHTML); // empty
}
});
</script>
<site-info>w3cdoc</site-info>
</body>
</html>
If running it, the alert is empty. (如果运行它,则警报为空。)
The reason is that no children exist on that stage, and the DOM is not finished. In case you intend to pass information to the custom element, attributes may be used. And, in case the children are needed, access can be deferred to them using the zero-delay setTimeout. (原因是该舞台上不存在子级,并且DOM尚未完成。 如果您打算将信息传递给自定义元素,则可以使用属性。 而且,如果需要子项,可以使用零延迟setTimeout将访问权限延迟给子项。)
It will operate as follows:
<!DOCTYPE html>
<html>
<head>
<title>Title of the document</title>
</head>
<body>
<script>
customElements.define('site-info', class extends HTMLElement {
connectedCallback() {
setTimeout(() => alert(this.innerHTML)); // w3cdoc
}
});
</script>
<site-info>w3cdoc</site-info>
</body>
</html>
However, the solution above is not perfect. In case the custom elements also use setTimeout for initializing themselves, then in the queue, the setTimeout will occur first, and the inner one will follow it. (但是,上述解决方案并不完美。如果自定义元素也使用setTimeout来初始化自己,则在队列中, setTimeout将首先发生,内部元素将跟随它。)
So, the inner element finishes initialization after the outer one. (因此,内部元素在外部元素之后完成初始化。)
Here is an example:
<!DOCTYPE html>
<html>
<head>
<title>Title of the document</title>
</head>
<body>
<script>
customElements.define('site-info', class extends HTMLElement {
connectedCallback() {
alert(`${this.id} connected.`);
setTimeout(() => alert(`${this.id} initialized.`));
}
});
</script>
<site-info id="outer">
<site-info id="inner"></site-info>
</site-info>
</body>
</html>
The output order looks like this:
outer connected. inner connected. outer initialized. inner initialized. (外部连接。 内部连接。 外部已初始化。 内部已初始化。)
Customized Built-in Elements
Customized Built-in Elements (定制内置元素)
Built-in HTML elements can be extended and customized by inheriting from their classes. (内置HTML元素可以通过从其类继承来扩展和自定义。)
For instance, let’s try to extend HTMLButtonElement:
class WelcomeButton extends HTMLButtonElement { /* custom element methods */ }
Then, it’s necessary to provide a third argument customElements.define, indicating the tag, like this:
customElements.define('hello-button', WelcomeButton, {
extends: 'button'
});
Different tags sharing the same DOM-class can exist. That’s why extends is necessary. (可以存在共享相同DOM类的不同标签。这就是为什么延期是必要的。)
And, finally, it’s required to insert a regular <button> tag and insert is=“welcome-button” to it as follows:
<button is="welcome-button">...</button>
The full example will look like this:
<!DOCTYPE html>
<html>
<head>
<title>Title of the document</title>
</head>
<body>
<script>
// The button that says "welcome" on click
(//点击时显示“欢迎”的按钮)
class WelcomeButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => alert("welcome"));
}
}
customElements.define('welcome-button', WelcomeButton, {
extends: 'button'
});
</script>
<button is="welcome-button">Click me</button>
<button is="welcome-button" disabled>Disabled</button>
</body>
</html>
Summary
Summary (概要)
In this chapter, we discovered two types of custom elements: Autonomous and Customized built-in elements.
Autonomous custom elements are “all-new” elements that extend the abstract HTMLElement class. (自主自定义元素是扩展抽象HTMLElement类的“全新”元素。)
The customized built-in elements are the extensions of the existing elements. Among browsers, the custom elements are well-supported. (自定义的内置元素是现有元素的扩展。 在浏览器中,自定义元素得到了很好的支持。)