Files
polymer/lib/legacy/class.html
T

526 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 noCopyProps = Object.assign({
behaviors: true
}, metaProps);
const filteredProps = Object.assign({
listeners: true,
hostAttributes: true
}, metaProps);
function copyProperties(source, target) {
for (let p in source) {
// NOTE: cannot copy `noCopyProps` methods onto prototype at least because
// `super.ready` must be called and is not included in the user fn.
if (!(p in noCopyProps)) {
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) {
return GenerateClassFromInfo({}, Polymer.LegacyElementMixin(klass), behaviors);
}
// 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 copyAndFilterBehaviors(proto, behaviors, lifecycle) {
for (let i=0; i<behaviors.length; i++) {
copyAndFilterProperties(proto, behaviors[i], lifecycle);
}
return lifecycle;
}
function copyAndFilterProperties(proto, infoOrBehavior, lifecycle) {
copyProperties(infoOrBehavior, proto);
for (let p in filteredProps) {
if (infoOrBehavior[p]) {
lifecycle[p] = lifecycle[p] || [];
lifecycle[p].push(infoOrBehavior[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);
}
}
}
// Note, the properties in `b` are normalized before being merged
// into `a`. This means that given `a == {}` and `b == {foo: String}`,
// the result is `a == {foo: {type: String}}`.
function mergeElementProperties(a, b) {
for (let p in b) {
a[p] = a[p] || {};
mergePropertyInfo(a[p], b[p]);
}
}
/* Note about construction and extension of legacy classes.
[Changed in Q4 2018 to optimize performance.]
When calling `Polymer` or `mixinBehaviors`, the generated class below is
made. The list of behaviors was previously made into one generated class per
behavior, but this is no longer the case as behaviors are now called
manually. Note, there may *still* be multiple generated classes in the
element's prototype chain if extension is used with `mixinBehaviors`.
The generated class is directly tied to the info object and behaviors
used to create it. That list of behaviors is filtered so it's only the
behaviors not active on the superclass. In order to call through to the
entire list of lifecycle methods, it's important to call `super`.
The element's `properties`, `observers`, and the `_registered` method
are controlled via the finalization mechanism provided by `Properties-Mixin`.
`Properties` and `observers` are collected by manually traversing the prototype
chain and merging.
The `_registered` method is called via `LegacyElementMixin._finalizeClass`
and is called on each prototype in the element's chain. Because a non-legacy
element may extend a legacy one, it's important that work in `_registered`
carefully act only once.
*/
/**
* @param {!PolymerInit} info Polymer info object
* @param {function(new:HTMLElement)} Base base class to extend with info object
* @param {Object} behaviors behaviors to copy into the element
* @return {function(new:HTMLElement)} Generated class
* @suppress {checkTypes}
* @private
*/
function GenerateClassFromInfo(info, Base, behaviors) {
// manages behavior and lifecycle processing (filled in after class definition)
let registered = false;
let activeBehaviors;
const lifecycle = {};
/** @private */
class PolymerGenerated extends Base {
static get properties() {
const properties = {};
if (activeBehaviors) {
for (let i=0, b; i < activeBehaviors.length; i++) {
b = activeBehaviors[i];
mergeElementProperties(properties, b.properties);
}
}
mergeElementProperties(properties, info.properties);
return properties;
}
static get observers() {
let observers = [];
if (activeBehaviors) {
for (let i=0, b; i < activeBehaviors.length; i++) {
b = activeBehaviors[i];
if (b.observers) {
observers = observers.concat(b.observers);
}
}
}
if (info.observers) {
observers = observers.concat(info.observers);
}
return observers;
}
/**
* @return {void}
*/
created() {
super.created();
const list = lifecycle.created;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
}
/**
* @return {void}
*/
// Called on element prototype
_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 = this;
// NOTE: this `registered` flag is required so that extensions
// that do not override `_registered` do not try to "re-register"
// this data. Only extensions that use `mixinBehaviors` will normally
// have this implementation.
if (!registered) {
registered = true;
if (activeBehaviors) {
copyAndFilterBehaviors(proto, activeBehaviors, lifecycle);
}
copyAndFilterProperties(proto, info, lifecycle);
let list = lifecycle.beforeRegister;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(proto);
}
}
list = lifecycle.registered;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(proto);
}
}
}
}
/**
* @return {void}
*/
_applyListeners() {
super._applyListeners();
const list = lifecycle.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]);
}
}
}
}
}
// 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() {
const list = lifecycle.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]);
}
}
}
super._ensureAttributes();
}
/**
* @return {void}
*/
ready() {
super.ready();
let list = lifecycle.ready;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
}
/**
* @return {void}
*/
attached() {
super.attached();
let list = lifecycle.attached;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this);
}
}
}
/**
* @return {void}
*/
detached() {
super.detached();
let list = lifecycle.detached;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].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) {
super.attributeChanged();
let list = lifecycle.attributeChanged;
if (list) {
for (let i=0; i < list.length; i++) {
list[i].call(this, name, old, value);
}
}
}
}
// apply behaviors, note actual copying is done lazily at first instance creation
if (behaviors) {
// 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.
if (!Array.isArray(behaviors)) {
behaviors = [behaviors];
}
let superBehaviors = Base.prototype.behaviors;
// get flattened, deduped list of behaviors *not* already on super class
activeBehaviors = flattenBehaviors(behaviors, null, superBehaviors);
PolymerGenerated.prototype.behaviors = superBehaviors ?
superBehaviors.concat(behaviors) : activeBehaviors;
}
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.
* @template T
* @param {function(T):T} mixin Optional mixin to apply to legacy base class
* before extending with Polymer metaprogramming.
* @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);
klass = GenerateClassFromInfo(info, klass, info.behaviors);
// decorate klass with registration info
klass.is = info.is;
// To match 1.x behavior, `_template` supplied on a behavior should take
// precedence over dom-module lookup for `is`; this also prevents
// dom-module injection under strictTemplatePolicy for an element that
// would normally get its template from a behavior.
// TODO(sorvell): Remove once "flattened behaviors" lands, since both
// `_template` and `is` will be on the same class after that change
if (klass.prototype._template !== undefined) {
klass.prototype._template = klass.prototype._template;
}
return klass;
};
Polymer.mixinBehaviors = mixinBehaviors;
})();
</script>