自定义元素为 Web 提供了一套组件模型。自定义元素规范提供:

  • 将类与自定义元素名称关联的机制。
  • 当自定义元素的实例更改状态(例如,从文档添加或删除)时调用的一组生命周期回调。
  • 当实例上的一组特定属性之一更改时,将被调用的一个回调。

Put together, these features let you build an element with its own public API that reacts to state changes. Polymer provides a set of features on top of the basic custom element specification.

本文档提供了一份与 Polymer 有关的自定义元素的概述。有关自定义元素的更详细的概述,请参阅: 自定义元素 v1: 可重用的 Web 组件 于 Web 基础集。

To define a custom element, you create an ES6 class and associate it with the custom element name. For the full set of Polymer features, extend the PolymerElement class:

import {PolymerElement} from '@polymer/polymer/polymer-element.js';

  export class MyPolymerElement extends PolymerElement {
    ...
  }

  customElements.define('my-polymer-element', MyPolymerElement);

Exporting the custom element class is optional, but recommended.

Import the element into an HTML file using <script type="module">.
Use the import statement (as shown above) to import it from another ES6 module.

<script type="module" src="./my-polymer-element.js">

Once you've imported it, you can use a custom element just like you'd use a standard element.

The element's class defines its behavior and public API.

自定义元素名称。 按照规范,自定义元素的名称 必须以小写 ASCII 字母开头,并且必须包含连字符(-)。 还有一个与已存在的元素名称冲突的被禁用的名称的简短列表。 有关详细信息,请参阅 HTML 规范中的 自定义元素核心概念 章节。

Polymer 为基本的自定义元素添加了一组功能:

  • 用于处理常见任务的实例方法。
  • 用于处理属物和属性的自动化,例如基于相应属性来设置属物。
  • 基于提供的 <template> 为元素实例创建阴影 DOM 树。
  • 支持数据绑定,属物变更观察者和被计算的属物的数据系统。

The PolymerElement class is made up of a set of class expression mixins that add individual features. You can also use these mixins individually if you want to use a subset of Polymer's features. See the API documentation for a list of individual mixins.

Polymer elements follow the standard lifecycle for custom elements. The custom element spec provides a set of callbacks called "custom element reactions" that allow you to run user code in response to certain lifecycle changes.

For performance, Polymer defers creating an element's shadow tree and initializing its data system until the first time the element is attached to the DOM. Polymer adds its own ready callback for this initialization.

Reaction Description
constructor Called when the element is upgraded (that is, when an element is created, or when a previously-created element becomes defined). The constructor is a logical place to set default values, and to manually set up event listeners for the element itself.
connectedCallback Called when the element is added to a document. Can be called multiple times during the lifetime of an element.

Uses include adding document-level event listeners. (For listeners local to the element, you can use annotated event listeners.)

disconnectedCallback Called when the element is removed from a document. Can be called multiple times during the lifetime of an element.

Uses include removing event listeners added in connectedCallback.

ready Called during Polymer-specific element initialization. Called once, the first time the element is attached to the document. For details, see Polymer element initialization.
attributeChangedCallback Called when any of the element's attributes are changed, appended, removed, or replaced.

Use to handle attribute changes that don't correspond to declared properties. (For declared properties, Polymer handles attribute changes automatically as described in attribute deserialization.)

For each reaction, the first line of your implementation must be a call to the superclass constructor or reaction. For the constructor, this is simply the super() call.

constructor() {
  super();
  // …
}

For other reactions, call the superclass method. This is required so Polymer can hook into the element's lifecycle.

connectedCallback() {
  super.connectedCallback();
  // …
}

The element constructor has a few special limitations:

  • The first statement in the constructor body must be a parameter-less call to the super method.
  • The constructor can't include a return statement, unless it is a simple early return (return or return this).
  • The constructor can't examine the element's attributes or children, and the constructor can't add attributes or children.

For a complete list of limitations, see Requirements for custom element constructors in the WHATWG HTML Specification.

Whenever possible, defer work until the connectedCallback or later instead of performing it in the constructor. See Defer non-critical work for some suggestions.

The custom elements specification doesn't provide a one-time initialization callback. Polymer provides a ready callback, invoked the first time the element is added to the DOM. (If the element is upgraded when it's already in the document, ready runs when the element is upgraded.)

ready() {
  super.ready();
  // do something that requires access to the shadow tree
  ... 

}

The PolymerElement class initializes your element's template and data system during the ready callback, so if you override ready, you must call super.ready() before accessing the element's shadow tree.

Polymer does several things at ready time:

  • Creates and attaches the element's shadow DOM tree.
  • Initializes the data system, propagating intial values to data bindings.
  • Allows observers and computed properties to run (as soon as any of their dependencies are defined).

When the superclass ready method returns, the element's template has been instantiated and initial property values have been set. However, light DOM elements may not have been distributed when ready is called.

Don't use ready to initialize an element based on dynamic values, like property values or an element's light DOM children. Instead, use observers to react to property changes, and observeNodes or the slotchange event to react to children being added and removed from the element.

Related topics:

When possible, defer work until after first paint. The render-status module provides an afterNextRender utility for this purpose.

import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';


class DeferElement extends PolymerElement {
  ...
  constructor() {
    super();
    // When possible, use afterNextRender to defer non-critical
    // work until after first paint.
    afterNextRender(this, function() {
      this.addEventListener('click', this._handleClick);
    });
  }
}

In most cases, you can call afterNextRender from either the constructor or the ready callback with similar results. For anything requiring access to the element's shadow tree, use the ready callback.

By specification, custom elements can be used before they're defined. Adding a definition for an element causes any existing instances of that element to be upgraded to the custom class.

For example, consider the following code:

<my-element></my-element>

<!-- load the elment definition -->
<script type="module" src="my-element.js">

When parsing this page, the browser will create an instance of <my-element> before parsing and executing the script. In this case, the element is created as an instance of HTMLElement, not MyElement. After the element is defined, the <my-element> instance is upgraded so it has the correct class (MyElement). The class constructor is called during the upgrade process, followed by any pending lifecycle callbacks.

Element upgrades allow you to place elements in the DOM while deferring the cost of initializing them. It's a progressive enhancement feature.

To avoid unstyled content, you can apply styles to undefined elements. See Style undefined elements for details.

In addition to PolymerElement, a custom element can extend another custom element:

import {MyElment} from './my-element.js';

export class ExtendedElement extends MyElement {
  static get is() { return 'extended-element'; }

  static get properties() {
    return {
      thingCount: {
        value: 0,
        observer: '_thingCountChanged'
      }
    }
  }
  _thingCountChanged() {
    console.log(`thing count is ${this.thingCount}`);
  }
};

customElements.define(ExtendedElement.is, ExtendedElement);

Polymer does not currently support extending built-in elements. The custom elements spec provides a mechanism for extending built-in elements, such as <button> and <input>. The spec calls these elements customized built-in elements. Customized built-in elements provide many advantages (for example, being able to take advantage of built-in accessibility features of UI elements like <button> and <input>). However, not all browser makers have agreed to support customized built-in elements, so Polymer does not support them at this time.

When you extend custom elements, Polymer treats the properties object and observers array specially: when instantiating an element, Polymer walks the prototype chain and flattens these objects. So the properties and observers of a subclass are added to those defined by the superclass.

A subclass can also inherit a template from its superclass. For details, see Inherit a template from another Polymer element.

To make it easy to extend your elements, the module that defines the element should export it:

export class MyElement extends PolymerElement { ... }

Legacy elements—elements defined using the legacy Polymer() function—don't require you to define your own class. So if you're extending a legacy element, like one of the Polymer paper elements, the module may not export a class.

If you're extending a legacy Polymer element, or a module that doesn't export the element, you can use the customElements.get method to retrieve the constructor for any custom element that's been defined.

// Import a legacy component
import './legacy-button.js';
// Retrieve the legacy-button constructor
const LegacyButton = customElements.get('legacy-button');
// Extend it!
export class MyExtendedButton extends LegacyButton { ... }

ES6 classes allow single inheritance, which can make it challenging to share code between unrelated elements. Class expression mixins let you share code between elements without adding a common superclass.

A class expression mixin is basically a function that operates as a class factory. You pass in a superclass, and the function generates a new class that extends the superclass with the mixin's methods.

const fancyDogClass = FancyMixin(dogClass);
const fancyCatClass = FancyMixin(catClass);

Add a mixin to your element like this:

class MyElement extends MyMixin(PolymerElement) {
  static get is() { return 'my-element' }
}

If that isn't clear, it may help to see it in two steps:

// Create new base class that adds MyMixin's methods to Polymer.Element
const PolymerElementPlusMixin = MyMixin(PolymerElement);

// Extend the new base class
class MyElement extends PolymerElementPlusMixin {
  static get is() { return 'my-element' }
}

So the inheritance hierarchy is:

MyElement <= PolymerElementPlusMixin <= PolymerElement

You can apply mixins to any element class, not just PolymerElement:

class MyExtendedElement extends SomeMixin(MyElement) {
  ...
}

You can also apply multiple mixins in sequence:

class AnotherElement extends AnotherMixin(MyMixin(PolymerElement)) { … }

A mixin is simply a function that takes a class and returns a subclass:

MyMixin = function(superClass) {
  return class extends superClass {
    constructor() {
      super();
      this.addEventListener('keypress', (e) => this._handlePress(e));
    }

    static get properties() {
      return {
        bar: {
          type: Object
        }
      };
    }

    static get observers() {
      return [ '_barChanged(bar.*)' ];
    }

    _barChanged(bar) { ... }

    _handlePress(e) { console.log('key pressed: ' + e.charCode); }
  }
}

Or using an ES6 arrow function:

MyMixin = (superClass) => class extends superClass {
  ...
}

The mixin class can define properties, observers, and methods just like a regular element class. In addition, a mixin can incorporate other mixins:

MyCompositeMixin = (base) => class extends MyMixin2(MyMixin1(base)) {
  ...
}

Because mixins are simply adding classes to the inheritance chain, all of the usual rules of inheritance apply. For example, mixin classes can define constructors, can call superclass methods with super, and so on.

Document your mixins. The Polymer build and lint tools require some extra documentation tags to property analyze mixins and elements that use them. Without the documentation tags, the tools will log warnings. For details on documenting mixins, see Class mixins in Document your elements.

When creating a mixin that you intend to share with other groups or publish, a couple of additional steps are recommended:

  • Use the dedupingMixin function to produce a mixin that can only be applied once.

  • Define the mixin in an ES module and export it.

The dedupingMixin function is useful because a mixin that's used by other mixins may accidentally be applied more than once. For example if MixinA includes MixinB and MixinC, and you create an element that uses MixinA but also uses MixinB directly:

class MyElement extends MixinB(MixinA(Polymer.Element)) { ... }

At this point, your element contains two copies of MixinB in its prototype chain. dedupingMixin takes a mixin function as an argument, and returns a new, deduplicating mixin function:

mixin-b.js

import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin.js';

// define the mixin
let internalMixinB = (base) =>
  class extends base {
    ...
  }

// deduplicate and export it
export const MixinB = dedupingMixin(internalMixinB);

Using the mixin

import {mixinB} from './mixin-b.js';

class Foo extends MixinB(PolymerElement) { ... }

The deduping mixin has two advantages: first, whenever you use the mixin, it memoizes the generated class, so any subsequent uses on the same base class return the same class object—a minor optimization.

More importantly, the deduping mixin checks whether this mixin has already been applied anywhere in the base class's prototype chain. If it has, the mixin simply returns the base class. In the example above, if you used dedupingMixinB instead of mixinB in both places, the mixin would only be applied once.

More information: Custom elements v1: reusable web components on Web Fundamentals.