Custom Element
简介
HTML 标准定义的网页元素,有时并不符合我们的需要,这时浏览器允许用户自定义网页元素,这就叫做 Custom Element。简单说,它就是用户自定义的网页元素,是 Web components 技术的核心。
举例来说,你可以自定义一个叫做<my-element>
的网页元素。
<my-element></my-element>
注意,自定义网页元素的标签名必须含有连字符-
,一个或多个连字符都可以。这是因为浏览器内置的的 HTML 元素标签名,都不含有连字符,这样可以做到有效区分。
下面的代码先定义一个自定义元素的类。
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow( { mode: 'open' } );
this.shadowRoot.innerHTML = `
<style>
/* scoped styles */
</style>
<slot></slot>
`;
}
static get observedAttributes() {
// Return list of attributes to watch.
}
attributeChangedCallback( name, oldValue, newValue ) {
// Run functionality when one of these attributes is changed.
}
connectedCallback() {
// Run functionality when an instance of this element is inserted into the DOM.
}
disconnectedCallback() {
// Run functionality when an instance of this element is removed from the DOM.
}
}
上面代码有几个注意点。
- 自定义元素类的基类是
HTMLElement
。当然也可以根据需要,基于HTMLElement
的子类,比如HTMLButtonElement
。 - 构造函数内部定义了 Shadow DOM。所谓
Shadow DOM
指的是,这部分的 HTML 代码和样式,不直接暴露给用户。 - 类可以定义生命周期方法,比如
connectedCallback()
。
然后,window.customElements.define()
方法,用来登记自定义元素与这个类之间的映射。
window.customElements.define('my-element', MyElement);
登记以后,页面上的每一个<my-element>
元素都是一个MyElement
类的实例。只要浏览器解析到<my-element>
元素,就会运行MyElement
的构造函数。
注意,如果没有登记就使用 Custom Element,浏览器会认为这是一个不认识的元素,会当做空的 div 元素处理。
window.customElements.define()
方法定义了 Custom Element 以后,可以使用window.customeElements.get()
方法获取该元素的构造方法。这使得除了直接插入 HTML 网页,Custom Element 也能使用脚本插入网页。
window.customElements.define(
'my-element',
class extends HTMLElement {...}
);
const el = window.customElements.get('my-element');
const myElement = new el();
document.body.appendChild(myElement);
如果你想扩展现有的 HTML 元素(比如<button>
)也是可以的。
class GreetingElement extends HTMLButtonElement
登记的时候,需要提供扩展的元素。
customElements.define('hey-there', GreetingElement, { extends: 'button' });
使用的时候,为元素加上is
属性就可以了。
<button is="hey-there" name="World">Howdy</button>
生命周期方法
Custom Element 提供一些生命周期方法。
class MyElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// here the element has been inserted into the DOM
}
}
上面代码中,connectedCallback()
方法就是MyElement
元素的生命周期方法。每次,该元素插入 DOM,就会自动执行该方法。
connectedCallback()
:插入 DOM 时调用。这可能不止一次发生,比如元素被移除后又重新添加。类的设置应该尽量放到这个方法里面执行,因为这时各种属性和子元素都可用。disconnectedCallback()
:移出 DOM 时执行。attributeChangedCallback(attrName, oldVal, newVal)
:添加、删除、更新或替换属性时调用。元素创建或升级时,也会调用。注意:只有加入observedAttributes
的属性才会执行这个方法。adoptedCallback()
:自定义元素移动到新的 document 时调用,比如执行document.adoptNode(element)
时。
下面是一个例子。
class GreetingElement extends HTMLElement {
constructor() {
super();
this._name = 'Stranger';
}
connectedCallback() {
this.addEventListener('click', e => alert(`Hello, ${this._name}!`));
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === 'name') {
if (newValue) {
this._name = newValue;
} else {
this._name = 'Stranger';
}
}
}
}
GreetingElement.observedAttributes = ['name'];
customElements.define('hey-there', GreetingElement);
上面代码中,GreetingElement.observedAttributes
属性用来指定白名单里面的属性,上例是name
属性。只要这个属性的值发生变化,就会自动调用attributeChangedCallback
方法。
使用上面这个类的方法如下。
<hey-there>Greeting</hey-there>
<hey-there name="Potch">Personalized Greeting</hey-there>
attributeChangedCallback
方法主要用于外部传入的属性,就像上面例子中name="Potch"
。
生命周期方法调用的顺序如下:constructor
-> attributeChangedCallback
-> connectedCallback
,即attributeChangedCallback
早于connectedCallback
执行。这是因为attributeChangedCallback
相当于调整配置,应该在插入 DOM 之前完成。
下面的例子能够更明显地看出这一点,在插入 DOM 前修改 Custome Element 的颜色。
class MyElement extends HTMLElement {
constructor() {
this.container = this.shadowRoot.querySelector('#container');
}
attributeChangedCallback(attr, oldVal, newVal) {
if(attr === 'disabled') {
if(this.hasAttribute('disabled') {
this.container.style.background = '#808080';
} else {
this.container.style.background = '#ffffff';
}
}
}
}
自定义属性和方法
Custom Element 允许自定义属性或方法。
class MyElement extends HTMLElement {
...
doSomething() {
// do something in this method
}
}
上面代码中,doSomething()
就是MyElement
的自定义方法,使用方法如下。
const element = document.querySelector('my-element');
element.doSomething();
自定义属性可以使用 JavaScript class 的所有语法,因此也可以设置取值器和赋值器。
class MyElement extends HTMLElement {
...
set disabled(isDisabled) {
if(isDisabled) {
this.setAttribute('disabled', '');
}
else {
this.removeAttribute('disabled');
}
}
get disabled() {
return this.hasAttribute('disabled');
}
}
上面代码中的取值器和赋值器,可用于<my-input name="name" disabled>
这样的用法。
window.customElements.whenDefined()
window.customElements.whenDefined()
方法在一个 Custom Element 被customElements.define()
方法定义以后执行,用于“升级”一个元素。
window.customElements.whenDefined('my-element')
.then(() => {
// my-element is now defined
})
如果某个属性值发生变化时,需要做出反应,可以将它放入observedAttributes
数组。
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['disabled'];
}
constructor() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
.disabled {
opacity: 0.4;
}
</style>
<div id="container"></div>
`;
this.container = this.shadowRoot('#container');
}
attributeChangedCallback(attr, oldVal, newVal) {
if(attr === 'disabled') {
if(this.disabled) {
this.container.classList.add('disabled');
}
else {
this.container.classList.remove('disabled')
}
}
}
}
回调函数
自定义元素的原型有一些属性,用来指定回调函数,在特定事件发生时触发。
- createdCallback:实例生成时触发
- attachedCallback:实例插入HTML文档时触发
- detachedCallback:实例从HTML文档移除时触发
- attributeChangedCallback(attrName, oldVal, newVal):实例的属性发生改变时(添加、移除、更新)触发
下面是一个例子。
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
console.log('created');
this.innerHTML = 'This is a my-demo element!';
};
proto.attachedCallback = function() {
console.log('attached');
};
var XFoo = document.registerElement('x-foo', {prototype: proto});
利用回调函数,可以方便地在自定义元素中插入HTML语句。
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
};
var XFoo = document.registerElement('x-foo-with-markup',
{prototype: XFooProto});
上面代码定义了createdCallback回调函数,生成实例时,该函数运行,插入如下的HTML语句。
<x-foo-with-markup>
<b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
Custom Element 的子元素
用户使用 Custom Element 时候,可以在内部放置子元素。Custom Element 提供<slot>
用来引用内部内容。
下面的<image-gallery>
是一个 Custom Element。用户在里面放置了子元素。
<image-gallery>
<img src="foo.jpg" slot="image">
<img src="bar.jpg" slot="image">
</image-gallery>
<image-gallery>
内部的模板如下。
<div id="container">
<div class="images">
<slot name="image"></slot>
</div>
</div>
最终合成的代码如下。
<div id="container">
<div class="images">
<slot name="image">
<img src="foo.jpg" slot="image">
<img src="bar.jpg" slot="image">
</slot>
</div>
</div>