Files
polymer/lib/legacy/class.html
T

530 lines
19 KiB
HTML

<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="legacy-element-mixin.html">
<script>
(function() {
'use strict';
const metaProps = {
attached: true,
detached: true,
ready: true,
created: true,
beforeRegister: true,
registered: true,
attributeChanged: true,
};
const noBehaviorCopyProps = Object.assign({
behaviors: true
}, metaProps);
const memoizedProps = Object.assign({
listeners: true,
hostAttributes: true
}, metaProps);
function copyProperties(source, target) {
for (let p in source) {
// NOTE: cannot copy `noBehaviorCopyProps` methods onto prototype at least because
// `super.ready` must be called and is not included in the user fn.
if (!(p in noBehaviorCopyProps)) {
let pd = Object.getOwnPropertyDescriptor(source, p);
if (pd) {
Object.defineProperty(target, p, pd);
}
}
}
}
/**
* Applies a "legacy" behavior or array of behaviors to the provided class.
*
* Note: this method will automatically also apply the `Polymer.LegacyElementMixin`
* to ensure that any legacy behaviors can rely on legacy Polymer API on
* the underlying element.
*
* @template T
* @param {!Object|!Array<!Object>} behaviors Behavior object or array of behaviors.
* @param {function(new:T)} klass Element class.
* @return {function(new:T)} Returns a new Element class extended by the
* passed in `behaviors` and also by `Polymer.LegacyElementMixin`.
* @memberof Polymer
* @suppress {invalidCasts, checkTypes}
*/
function mixinBehaviors(behaviors, klass) {
if (behaviors) {
klass = applyBehaviors(behaviors, klass);
}
// provides behaviors functionality
return GenerateClassFromInfo({}, klass);
}
function applyBehaviors(behaviors, klass) {
if (!behaviors) {
klass = /** @type {HTMLElement} */(klass); // eslint-disable-line no-self-assign
return klass;
}
// NOTE: ensure the behavior is extending a class with
// legacy element api. This is necessary since behaviors expect to be able
// to access 1.x legacy api.
klass = class extends Polymer.LegacyElementMixin(klass) { };
if (!Array.isArray(behaviors)) {
behaviors = [behaviors];
}
let superBehaviors = klass.prototype.behaviors;
// get flattened, deduped list of behaviors *not* already on super class
behaviors = flattenBehaviors(behaviors, null, superBehaviors);
if (superBehaviors) {
behaviors = superBehaviors.concat(behaviors);
}
// Set behaviors on prototype
klass.prototype.behaviors = behaviors;
return klass;
}
// NOTE:
// 1.x
// Behaviors were mixed in *in reverse order* and de-duped on the fly.
// The rule was that behavior properties were copied onto the element
// prototype if and only if the property did not already exist.
// Given: Polymer{ behaviors: [A, B, C, A, B]}, property copy order was:
// (1), B, (2), A, (3) C. This means prototype properties win over
// B properties win over A win over C. This mirrors what would happen
// with inheritance if element extended B extended A extended C.
//
// Again given, Polymer{ behaviors: [A, B, C, A, B]}, the resulting
// `behaviors` array was [C, A, B].
// Behavior lifecycle methods were called in behavior array order
// followed by the element, e.g. (1) C.created, (2) A.created,
// (3) B.created, (4) element.created. There was no support for
// super, and "super-behavior" methods were callable only by name).
//
// 2.x
// Behaviors are made into proper mixins which live in the
// element's prototype chain. Behaviors are placed in the element prototype
// eldest to youngest and de-duped youngest to oldest:
// So, first [A, B, C, A, B] becomes [C, A, B] then,
// the element prototype becomes (oldest) (1) Polymer.Element, (2) class(C),
// (3) class(A), (4) class(B), (5) class(Polymer({...})).
// Result:
// This means element properties win over B properties win over A win
// over C. (same as 1.x)
// If lifecycle is called (super then me), order is
// (1) C.created, (2) A.created, (3) B.created, (4) element.created
// (again same as 1.x)
function copyBehaviorProperties(behaviors, klass) {
const meta = {};
if (behaviors) {
klass.prototype.__behaviorMetaProps = meta;
for (let i=0; i<behaviors.length; i++) {
copyProperties(behaviors[i], klass.prototype);
memoizeBehaviorMetaProps(meta, behaviors[i]);
}
}
klass.prototype.__behaviorMetaProps = meta;
}
function memoizeBehaviorMetaProps(meta, behavior) {
for (let p in memoizedProps) {
if (behavior[p]) {
meta[p] = meta[p] || [];
meta[p].push(behavior[p]);
}
}
}
/**
* @param {Array} behaviors List of behaviors to flatten.
* @param {Array=} list Target list to flatten behaviors into.
* @param {Array=} exclude List of behaviors to exclude from the list.
* @return {!Array} Returns the list of flattened behaviors.
*/
function flattenBehaviors(behaviors, list, exclude) {
list = list || [];
for (let i=behaviors.length-1; i >= 0; i--) {
let b = behaviors[i];
if (b) {
if (Array.isArray(b)) {
flattenBehaviors(b, list);
} else {
// dedup
if (list.indexOf(b) < 0 && (!exclude || exclude.indexOf(b) < 0)) {
list.unshift(b);
}
}
} else {
console.warn('behavior is null, check for missing or 404 import');
}
}
return list;
}
function mergePropertyInfo(a, b) {
if (typeof b === 'function') {
a.type = b;
return;
}
if ('type' in b) {
a.type = b.type;
}
if ('value' in b) {
a.value = b.value;
}
// readOnly: cannot become false and takes on `computed` value
a.readOnly = a.readOnly || Boolean(a.computed) || b.readOnly || Boolean(b.computed);
// computed: first in wins
if ('computed' in b) {
a.computed = a.computed || b.computed;
}
// reflectToAttribute: cannot become false
if ('reflectToAttribute' in b) {
a.reflectToAttribute = a.reflectToAttribute || b.reflectToAttribute;
}
// notify: cannot become false (only install 1x)
if ('notify' in b) {
a.notify = a.notify || b.notify;
}
// make this an array
if ('observer' in b) {
if (!a.observer) {
a.observer = b.observer;
// we have both: make array and push onto
} else {
if (!Array.isArray(a.observer)) {
a.observer = [a.observer];
}
a.observer.push(b.observer);
}
}
}
function mergeProperties(a, b) {
for (let p in b) {
a[p] = a[p] || {};
mergePropertyInfo(a[p], b[p]);
}
}
/**
* @param {!PolymerInit} info Polymer info object
* @param {function(new:HTMLElement)} Base base class to extend with info object
* @return {function(new:HTMLElement)} Generated class
* @suppress {checkTypes}
* @private
*/
function GenerateClassFromInfo(info, Base) {
/** @private */
class PolymerGenerated extends Base {
static get properties() {
const properties = {};
if (this.prototype.behaviors) {
for (let i=0, b; i < this.prototype.behaviors.length; i++) {
b = this.prototype.behaviors[i];
mergeProperties(properties, b.properties);
}
}
mergeProperties(properties, info.properties);
return properties;
}
static get observers() {
let observers = [];
if (this.prototype.behaviors) {
for (let i=0, b; i < this.prototype.behaviors.length; i++) {
b = this.prototype.behaviors[i];
if (b.observers) {
observers = observers.concat(b.observers);
}
}
}
if (info.observers) {
observers = observers.concat(info.observers);
}
return observers;
}
/**
* @return {HTMLTemplateElement} template for this class
*/
static get template() {
// get template first from any imperative set in `info._template`
return info._template ||
// next look in dom-module associated with this element's is.
Polymer.DomModule && Polymer.DomModule.import(this.is, 'template') ||
// next look for superclass template (note: use superclass symbol
// to ensure correct `this.is`)
Base.template ||
// finally fall back to `_template` in element's prototype.
this.prototype._template ||
null;
}
/**
* @return {void}
*/
created() {
const list = this.__behaviorMetaProps.created;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
if (info.created) {
info.created.call(this);
}
}
/**
* @return {void}
*/
_registered() {
/* NOTE: `beforeRegister` is called here for bc, but the behavior
is different than in 1.x. In 1.0, the method was called *after*
mixing prototypes together but *before* processing of meta-objects.
However, dynamic effects can still be set here and can be done either
in `beforeRegister` or `registered`. It is no longer possible to set
`is` in `beforeRegister` as you could in 1.x.
*/
const proto = Object.getPrototypeOf(this);
copyBehaviorProperties(proto.behaviors, proto.constructor);
copyProperties(info, proto);
// Note, previously these were interleaved.
let list = this.__behaviorMetaProps.beforeRegister;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(proto);
}
}
list = this.__behaviorMetaProps.registered;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(proto);
}
}
if (info.beforeRegister) {
info.beforeRegister.call(proto);
}
if (info.registered) {
info.registered.call(proto);
}
}
/**
* @return {void}
*/
_applyListeners() {
const list = this.__behaviorMetaProps.listeners;
if (list) {
for (let i=0; i < list.length; i++) {
const listeners = list[i];
if (listeners) {
for (let l in listeners) {
this._addMethodEventListenerToNode(this, l, listeners[l]);
}
}
}
}
if (info.listeners) {
for (let l in info.listeners) {
this._addMethodEventListenerToNode(this, l, info.listeners[l]);
}
}
}
// note: exception to "super then me" rule;
// do work before calling super so that super attributes
// only apply if not already set.
/**
* @return {void}
*/
_ensureAttributes() {
if (info.hostAttributes) {
for (let a in info.hostAttributes) {
this._ensureAttribute(a, info.hostAttributes[a]);
}
}
const list = this.__behaviorMetaProps.hostAttributes;
if (list) {
for (let i=list.length-1; i >= 0; i--) {
const hostAttributes = list[i];
for (let a in hostAttributes) {
this._ensureAttribute(a, hostAttributes[a]);
}
}
}
}
/**
* @return {void}
*/
ready() {
super.ready();
let list = this.__behaviorMetaProps.ready;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
if (info.ready) {
info.ready.call(this);
}
}
/**
* @return {void}
*/
attached() {
let list = this.__behaviorMetaProps.attached;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
if (info.attached) {
info.attached.call(this);
}
}
/**
* @return {void}
*/
detached() {
let list = this.__behaviorMetaProps.detached;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
if (info.detached) {
info.detached.call(this);
}
}
/**
* Implements native Custom Elements `attributeChangedCallback` to
* set an attribute value to a property via `_attributeToProperty`.
*
* @param {string} name Name of attribute that changed
* @param {?string} old Old attribute value
* @param {?string} value New attribute value
* @return {void}
*/
attributeChanged(name, old, value) {
let list = this.__behaviorMetaProps.attributeChanged;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this, name, old, value);
}
}
if (info.attributeChanged) {
info.attributeChanged.call(this, name, old, value);
}
}
}
PolymerGenerated.generatedFrom = info;
return PolymerGenerated;
}
/**
* Generates a class that extends `Polymer.LegacyElement` based on the
* provided info object. Metadata objects on the `info` object
* (`properties`, `observers`, `listeners`, `behaviors`, `is`) are used
* for Polymer's meta-programming systems, and any functions are copied
* to the generated class.
*
* Valid "metadata" values are as follows:
*
* `is`: String providing the tag name to register the element under. In
* addition, if a `dom-module` with the same id exists, the first template
* in that `dom-module` will be stamped into the shadow root of this element,
* with support for declarative event listeners (`on-...`), Polymer data
* bindings (`[[...]]` and `{{...}}`), and id-based node finding into
* `this.$`.
*
* `properties`: Object describing property-related metadata used by Polymer
* features (key: property names, value: object containing property metadata).
* Valid keys in per-property metadata include:
* - `type` (String|Number|Object|Array|...): Used by
* `attributeChangedCallback` to determine how string-based attributes
* are deserialized to JavaScript property values.
* - `notify` (boolean): Causes a change in the property to fire a
* non-bubbling event called `<property>-changed`. Elements that have
* enabled two-way binding to the property use this event to observe changes.
* - `readOnly` (boolean): Creates a getter for the property, but no setter.
* To set a read-only property, use the private setter method
* `_setProperty(property, value)`.
* - `observer` (string): Observer method name that will be called when
* the property changes. The arguments of the method are
* `(value, previousValue)`.
* - `computed` (string): String describing method and dependent properties
* for computing the value of this property (e.g. `'computeFoo(bar, zot)'`).
* Computed properties are read-only by default and can only be changed
* via the return value of the computing method.
*
* `observers`: Array of strings describing multi-property observer methods
* and their dependent properties (e.g. `'observeABC(a, b, c)'`).
*
* `listeners`: Object describing event listeners to be added to each
* instance of this element (key: event name, value: method name).
*
* `behaviors`: Array of additional `info` objects containing metadata
* and callbacks in the same format as the `info` object here which are
* merged into this element.
*
* `hostAttributes`: Object listing attributes to be applied to the host
* once created (key: attribute name, value: attribute value). Values
* are serialized based on the type of the value. Host attributes should
* generally be limited to attributes such as `tabIndex` and `aria-...`.
* Attributes in `hostAttributes` are only applied if a user-supplied
* attribute is not already present (attributes in markup override
* `hostAttributes`).
*
* In addition, the following Polymer-specific callbacks may be provided:
* - `registered`: called after first instance of this element,
* - `created`: called during `constructor`
* - `attached`: called during `connectedCallback`
* - `detached`: called during `disconnectedCallback`
* - `ready`: called before first `attached`, after all properties of
* this element have been propagated to its template and all observers
* have run
*
* @param {!PolymerInit} info Object containing Polymer metadata and functions
* to become class methods.
* @return {function(new:HTMLElement)} Generated class
* @memberof Polymer
*/
Polymer.Class = function(info, mixin) {
if (!info) {
console.warn('Polymer.Class requires `info` argument');
}
let klass = mixin ? mixin(Polymer.LegacyElementMixin(HTMLElement)) :
Polymer.LegacyElementMixin(HTMLElement);
if (info.behaviors) {
klass = applyBehaviors(info.behaviors, klass);
}
klass = GenerateClassFromInfo(info, klass);
// decorate klass with registration info
klass.is = info.is;
return klass;
};
Polymer.mixinBehaviors = mixinBehaviors;
})();
</script>