Custom Element

简介

HTML 标准定义的网页元素,有时并不符合我们的需要,这时浏览器允许用户自定义网页元素,这就叫做 Custom Element。简单说,它就是用户自定义的网页元素,是 Web components 技术的核心。

举例来说,你可以自定义一个叫做<my-element>的网页元素。

  1. <my-element></my-element>

注意,自定义网页元素的标签名必须含有连字符-,一个或多个连字符都可以。这是因为浏览器内置的的 HTML 元素标签名,都不含有连字符,这样可以做到有效区分。

下面的代码先定义一个自定义元素的类。

  1. class MyElement extends HTMLElement {
  2. constructor() {
  3. super();
  4. this.attachShadow( { mode: 'open' } );
  5. this.shadowRoot.innerHTML = `
  6. <style>
  7. /* scoped styles */
  8. </style>
  9. <slot></slot>
  10. `;
  11. }
  12. static get observedAttributes() {
  13. // Return list of attributes to watch.
  14. }
  15. attributeChangedCallback( name, oldValue, newValue ) {
  16. // Run functionality when one of these attributes is changed.
  17. }
  18. connectedCallback() {
  19. // Run functionality when an instance of this element is inserted into the DOM.
  20. }
  21. disconnectedCallback() {
  22. // Run functionality when an instance of this element is removed from the DOM.
  23. }
  24. }

上面代码有几个注意点。

  • 自定义元素类的基类是HTMLElement。当然也可以根据需要,基于HTMLElement的子类,比如HTMLButtonElement
  • 构造函数内部定义了 Shadow DOM。所谓Shadow DOM指的是,这部分的 HTML 代码和样式,不直接暴露给用户。
  • 类可以定义生命周期方法,比如connectedCallback()

然后,window.customElements.define()方法,用来登记自定义元素与这个类之间的映射。

  1. 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 也能使用脚本插入网页。

  1. window.customElements.define(
  2. 'my-element',
  3. class extends HTMLElement {...}
  4. );
  5. const el = window.customElements.get('my-element');
  6. const myElement = new el();
  7. document.body.appendChild(myElement);

如果你想扩展现有的 HTML 元素(比如<button>)也是可以的。

  1. class GreetingElement extends HTMLButtonElement

登记的时候,需要提供扩展的元素。

  1. customElements.define('hey-there', GreetingElement, { extends: 'button' });

使用的时候,为元素加上is属性就可以了。

  1. <button is="hey-there" name="World">Howdy</button>

生命周期方法

Custom Element 提供一些生命周期方法。

  1. class MyElement extends HTMLElement {
  2. constructor() {
  3. super();
  4. }
  5. connectedCallback() {
  6. // here the element has been inserted into the DOM
  7. }
  8. }

上面代码中,connectedCallback()方法就是MyElement元素的生命周期方法。每次,该元素插入 DOM,就会自动执行该方法。

  • connectedCallback():插入 DOM 时调用。这可能不止一次发生,比如元素被移除后又重新添加。类的设置应该尽量放到这个方法里面执行,因为这时各种属性和子元素都可用。
  • disconnectedCallback():移出 DOM 时执行。
  • attributeChangedCallback(attrName, oldVal, newVal):添加、删除、更新或替换属性时调用。元素创建或升级时,也会调用。注意:只有加入observedAttributes的属性才会执行这个方法。
  • adoptedCallback():自定义元素移动到新的 document 时调用,比如执行document.adoptNode(element)时。

下面是一个例子。

  1. class GreetingElement extends HTMLElement {
  2. constructor() {
  3. super();
  4. this._name = 'Stranger';
  5. }
  6. connectedCallback() {
  7. this.addEventListener('click', e => alert(`Hello, ${this._name}!`));
  8. }
  9. attributeChangedCallback(attrName, oldValue, newValue) {
  10. if (attrName === 'name') {
  11. if (newValue) {
  12. this._name = newValue;
  13. } else {
  14. this._name = 'Stranger';
  15. }
  16. }
  17. }
  18. }
  19. GreetingElement.observedAttributes = ['name'];
  20. customElements.define('hey-there', GreetingElement);

上面代码中,GreetingElement.observedAttributes属性用来指定白名单里面的属性,上例是name属性。只要这个属性的值发生变化,就会自动调用attributeChangedCallback方法。

使用上面这个类的方法如下。

  1. <hey-there>Greeting</hey-there>
  2. <hey-there name="Potch">Personalized Greeting</hey-there>

attributeChangedCallback方法主要用于外部传入的属性,就像上面例子中name="Potch"

生命周期方法调用的顺序如下:constructor -> attributeChangedCallback -> connectedCallback,即attributeChangedCallback早于connectedCallback执行。这是因为attributeChangedCallback相当于调整配置,应该在插入 DOM 之前完成。

下面的例子能够更明显地看出这一点,在插入 DOM 前修改 Custome Element 的颜色。

  1. class MyElement extends HTMLElement {
  2. constructor() {
  3. this.container = this.shadowRoot.querySelector('#container');
  4. }
  5. attributeChangedCallback(attr, oldVal, newVal) {
  6. if(attr === 'disabled') {
  7. if(this.hasAttribute('disabled') {
  8. this.container.style.background = '#808080';
  9. } else {
  10. this.container.style.background = '#ffffff';
  11. }
  12. }
  13. }
  14. }

自定义属性和方法

Custom Element 允许自定义属性或方法。

  1. class MyElement extends HTMLElement {
  2. ...
  3. doSomething() {
  4. // do something in this method
  5. }
  6. }

上面代码中,doSomething()就是MyElement的自定义方法,使用方法如下。

  1. const element = document.querySelector('my-element');
  2. element.doSomething();

自定义属性可以使用 JavaScript class 的所有语法,因此也可以设置取值器和赋值器。

  1. class MyElement extends HTMLElement {
  2. ...
  3. set disabled(isDisabled) {
  4. if(isDisabled) {
  5. this.setAttribute('disabled', '');
  6. }
  7. else {
  8. this.removeAttribute('disabled');
  9. }
  10. }
  11. get disabled() {
  12. return this.hasAttribute('disabled');
  13. }
  14. }

上面代码中的取值器和赋值器,可用于<my-input name="name" disabled>这样的用法。

window.customElements.whenDefined()

window.customElements.whenDefined()方法在一个 Custom Element 被customElements.define()方法定义以后执行,用于“升级”一个元素。

  1. window.customElements.whenDefined('my-element')
  2. .then(() => {
  3. // my-element is now defined
  4. })

如果某个属性值发生变化时,需要做出反应,可以将它放入observedAttributes数组。

  1. class MyElement extends HTMLElement {
  2. static get observedAttributes() {
  3. return ['disabled'];
  4. }
  5. constructor() {
  6. const shadowRoot = this.attachShadow({mode: 'open'});
  7. shadowRoot.innerHTML = `
  8. <style>
  9. .disabled {
  10. opacity: 0.4;
  11. }
  12. </style>
  13. <div id="container"></div>
  14. `;
  15. this.container = this.shadowRoot('#container');
  16. }
  17. attributeChangedCallback(attr, oldVal, newVal) {
  18. if(attr === 'disabled') {
  19. if(this.disabled) {
  20. this.container.classList.add('disabled');
  21. }
  22. else {
  23. this.container.classList.remove('disabled')
  24. }
  25. }
  26. }
  27. }

回调函数

自定义元素的原型有一些属性,用来指定回调函数,在特定事件发生时触发。

  • createdCallback:实例生成时触发
  • attachedCallback:实例插入HTML文档时触发
  • detachedCallback:实例从HTML文档移除时触发
  • attributeChangedCallback(attrName, oldVal, newVal):实例的属性发生改变时(添加、移除、更新)触发

下面是一个例子。

  1. var proto = Object.create(HTMLElement.prototype);
  2. proto.createdCallback = function() {
  3. console.log('created');
  4. this.innerHTML = 'This is a my-demo element!';
  5. };
  6. proto.attachedCallback = function() {
  7. console.log('attached');
  8. };
  9. var XFoo = document.registerElement('x-foo', {prototype: proto});

利用回调函数,可以方便地在自定义元素中插入HTML语句。

  1. var XFooProto = Object.create(HTMLElement.prototype);
  2. XFooProto.createdCallback = function() {
  3. this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  4. };
  5. var XFoo = document.registerElement('x-foo-with-markup',
  6. {prototype: XFooProto});

上面代码定义了createdCallback回调函数,生成实例时,该函数运行,插入如下的HTML语句。

  1. <x-foo-with-markup>
  2. <b>I'm an x-foo-with-markup!</b>
  3. </x-foo-with-markup>

Custom Element 的子元素

用户使用 Custom Element 时候,可以在内部放置子元素。Custom Element 提供<slot>用来引用内部内容。

下面的<image-gallery>是一个 Custom Element。用户在里面放置了子元素。

  1. <image-gallery>
  2. <img src="foo.jpg" slot="image">
  3. <img src="bar.jpg" slot="image">
  4. </image-gallery>

<image-gallery>内部的模板如下。

  1. <div id="container">
  2. <div class="images">
  3. <slot name="image"></slot>
  4. </div>
  5. </div>

最终合成的代码如下。

  1. <div id="container">
  2. <div class="images">
  3. <slot name="image">
  4. <img src="foo.jpg" slot="image">
  5. <img src="bar.jpg" slot="image">
  6. </slot>
  7. </div>
  8. </div>