diff --git a/lib/legacy/class.js b/lib/legacy/class.js index 65ca95b1e8..e1717f6d8a 100644 --- a/lib/legacy/class.js +++ b/lib/legacy/class.js @@ -11,7 +11,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN import { LegacyElementMixin } from './legacy-element-mixin.js'; import { DomModule } from '../elements/dom-module.js'; -let metaProps = { +const metaProps = { attached: true, detached: true, ready: true, @@ -19,15 +19,22 @@ let metaProps = { beforeRegister: true, registered: true, attributeChanged: true, - // meta objects - behaviors: 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 `metaProps` methods onto prototype at least because + // 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 metaProps)) { + if (!(p in noBehaviorCopyProps)) { let pd = Object.getOwnPropertyDescriptor(source, p); if (pd) { Object.defineProperty(target, p, pd); @@ -36,7 +43,6 @@ function copyProperties(source, target) { } } -// TODO(sorvell): this breaks `Polymer.mixinBehaviors`; should fix via factoring /** * Applies a "legacy" behavior or array of behaviors to the provided class. * @@ -53,37 +59,7 @@ function copyProperties(source, target) { * @suppress {invalidCasts, checkTypes} */ export 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 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); - // mixin new behaviors - klass = _applyBehaviors(behaviors, klass); - if (superBehaviors) { - behaviors = superBehaviors.concat(behaviors); - } - // Set behaviors on prototype for BC... - klass.prototype.behaviors = behaviors; - return klass; + return GenerateClassFromInfo({}, LegacyElementMixin(klass), behaviors); } // NOTE: @@ -116,15 +92,26 @@ function applyBehaviors(behaviors, klass) { // 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 _applyBehaviors(behaviors, klass) { - for (let i=0; i= 0; i--) { - b = this.behaviors[i]; - if (b.hostAttributes) { - for (let a in b.hostAttributes) { - this._ensureAttribute(a, b.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]); } - } } } } @@ -336,12 +330,10 @@ function GenerateClassFromInfo(info, Base) { */ ready() { super.ready(); - if (this.behaviors) { - for (let i=0, b; i < this.behaviors.length; i++) { - b = this.behaviors[i]; - if (b.ready) { - b.ready.call(this); - } + let list = this.__behaviorMetaProps.ready; + if (list) { + for (let i=0; i < list.length; i++) { + list[i].call(this); } } if (info.ready) { @@ -353,12 +345,10 @@ function GenerateClassFromInfo(info, Base) { * @return {void} */ attached() { - if (this.behaviors) { - for (let i=0, b; i < this.behaviors.length; i++) { - b = this.behaviors[i]; - if (b.attached) { - b.attached.call(this); - } + let list = this.__behaviorMetaProps.attached; + if (list) { + for (let i=0; i < list.length; i++) { + list[i].call(this); } } if (info.attached) { @@ -370,12 +360,10 @@ function GenerateClassFromInfo(info, Base) { * @return {void} */ detached() { - if (this.behaviors) { - for (let i=0, b; i < this.behaviors.length; i++) { - b = this.behaviors[i]; - if (b.detached) { - b.detached.call(this); - } + let list = this.__behaviorMetaProps.detached; + if (list) { + for (let i=0; i < list.length; i++) { + list[i].call(this); } } if (info.detached) { @@ -384,6 +372,7 @@ function GenerateClassFromInfo(info, Base) { } /** + * * Implements native Custom Elements `attributeChangedCallback` to * set an attribute value to a property via `_attributeToProperty`. * @@ -393,23 +382,36 @@ function GenerateClassFromInfo(info, Base) { * @return {void} */ attributeChanged(name, old, value) { - if (this.behaviors) { - for (let i=0, b; i < this.behaviors.length; i++) { - b = this.behaviors[i]; - if (b.attributeChanged) { - b.attributeChanged.call(this, 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; + // apply behaviors + 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 = PolymerGenerated.prototype.behaviors; + // get flattened, deduped list of behaviors *not* already on super class + behaviors = flattenBehaviors(behaviors, null, superBehaviors); + PolymerGenerated.prototype.behaviors = superBehaviors ? + superBehaviors.concat(behaviors) : behaviors; + PolymerGenerated.prototype.__behaviors = behaviors; + } - copyProperties(info, PolymerGenerated.prototype); + PolymerGenerated.generatedFrom = info; return PolymerGenerated; } @@ -486,14 +488,11 @@ function GenerateClassFromInfo(info, Base) { */ export const Class = function(info, mixin) { if (!info) { - console.warn(`Polymer's Class function requires \`info\` argument`); + console.warn('Polymer.Class requires `info` argument'); } let klass = mixin ? mixin(LegacyElementMixin(HTMLElement)) : LegacyElementMixin(HTMLElement); - if (info.behaviors) { - klass = applyBehaviors(info.behaviors, klass); - } - klass = GenerateClassFromInfo(info, klass); + klass = GenerateClassFromInfo(info, klass, info.behaviors); // decorate klass with registration info klass.is = info.is; return klass; diff --git a/lib/legacy/legacy-element-mixin.js b/lib/legacy/legacy-element-mixin.js index a6d00c1671..d3ffdcba7e 100644 --- a/lib/legacy/legacy-element-mixin.js +++ b/lib/legacy/legacy-element-mixin.js @@ -77,11 +77,6 @@ export const LegacyElementMixin = dedupingMixin((base) => { this.__boundListeners; /** @type {Object} */ this._debouncers; - // Ensure listeners are applied immediately so that they are - // added before declarative event listeners. This allows an element to - // decorate itself via an event prior to any declarative listeners - // seeing the event. Note, this ensures compatibility with 1.x ordering. - this._applyListeners(); } /** @@ -96,6 +91,11 @@ export const LegacyElementMixin = dedupingMixin((base) => { return this.prototype.importMeta; } + static _finalizeClass() { + this.prototype._registered(); + super._finalizeClass(); + } + /** * Legacy callback called during the `constructor`, for overriding * by the user. @@ -178,14 +178,14 @@ export const LegacyElementMixin = dedupingMixin((base) => { * @suppress {invalidCasts} */ _initializeProperties() { - let proto = Object.getPrototypeOf(this); - if (!proto.hasOwnProperty('__hasRegisterFinished')) { - proto.__hasRegisterFinished = true; - this._registered(); - } super._initializeProperties(); this.root = /** @type {HTMLElement} */(this); this.created(); + // Ensure listeners are applied immediately so that they are + // added before declarative event listeners. This allows an element to + // decorate itself via an event prior to any declarative listeners + // seeing the event. Note, this ensures compatibility with 1.x ordering. + this._applyListeners(); } /** diff --git a/lib/mixins/element-mixin.js b/lib/mixins/element-mixin.js index 8cca1a2bfd..35bbb6432b 100644 --- a/lib/mixins/element-mixin.js +++ b/lib/mixins/element-mixin.js @@ -16,7 +16,7 @@ import { pathFromUrl, resolveCss, resolveUrl } from '../utils/resolve-url.js'; import { DomModule } from '../elements/dom-module.js'; import { PropertyEffects } from './property-effects.js'; import { PropertiesMixin } from './properties-mixin.js'; -import { skipStyleIncludesAndUrls } from '../utils/settings.js'; +import { legacyOptimizations } from '../utils/settings.js'; /** * Current Polymer version in Semver notation. @@ -24,6 +24,8 @@ import { skipStyleIncludesAndUrls } from '../utils/settings.js'; */ export const version = '3.0.5'; +const builtCSS = window.ShadyCSS && window.ShadyCSS['cssBuild']; + /** * Element class mixin that provides the core API for Polymer's meta-programming * features including template stamping, data-binding, attribute deserialization, @@ -253,7 +255,7 @@ export const ElementMixin = dedupingMixin(base => { * @private */ function processElementStyles(klass, template, is, baseURI) { - if (!skipStyleIncludesAndUrls) { + if (!builtCSS) { const templateStyles = template.content.querySelectorAll('style'); const stylesWithImports = stylesFromTemplate(template); // insert styles from at the top of the template @@ -335,10 +337,6 @@ export const ElementMixin = dedupingMixin(base => { */ static _finalizeClass() { super._finalizeClass(); - if (this.hasOwnProperty( - JSCompiler_renameProperty('is', this)) && this.is) { - register(this.prototype); - } const observers = ownObservers(this); if (observers) { this.createObservers(observers, this._properties); @@ -349,7 +347,7 @@ export const ElementMixin = dedupingMixin(base => { if (typeof template === 'string') { console.error('template getter must return HTMLTemplateElement'); template = null; - } else { + } else if (!legacyOptimizations) { template = template.cloneNode(true); } } @@ -524,7 +522,6 @@ export const ElementMixin = dedupingMixin(base => { * @suppress {invalidCasts} */ _initializeProperties() { - instanceCount++; this.constructor.finalize(); // note: finalize template when we have access to `localName` to // avoid dependence on `is` for polyfilling styling. @@ -743,46 +740,6 @@ export const ElementMixin = dedupingMixin(base => { return PolymerElement; }); -/** - * Total number of Polymer element instances created. - * @type {number} - */ -export let instanceCount = 0; - -/** - * Array of Polymer element classes that have been finalized. - * @type {Array} - */ -export const registrations = []; - -/** - * @param {!PolymerElementConstructor} prototype Element prototype to log - * @this {this} - * @private - */ -function _regLog(prototype) { - console.log('[' + prototype.is + ']: registered'); -} - -/** - * Registers a class prototype for telemetry purposes. - * @param {HTMLElement} prototype Element prototype to register - * @this {this} - * @protected - */ -export function register(prototype) { - registrations.push(prototype); -} - -/** - * Logs all elements registered with an `is` to the console. - * @public - * @this {this} - */ -export function dumpRegistrations() { - registrations.forEach(_regLog); -} - /** * When using the ShadyCSS scoping and custom property shim, causes all * shimmed `styles` (via `custom-style`) in the document (and its subtree) diff --git a/lib/mixins/properties-mixin.js b/lib/mixins/properties-mixin.js index e9f558424b..871937bac2 100644 --- a/lib/mixins/properties-mixin.js +++ b/lib/mixins/properties-mixin.js @@ -10,6 +10,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN import '../utils/boot.js'; import { dedupingMixin } from '../utils/mixin.js'; +import { register, incrementInstanceCount } from '../utils/telemetry.js'; import { PropertiesChanged } from './properties-changed.js'; /** @@ -114,8 +115,12 @@ export const PropertiesMixin = dedupingMixin(superClass => { * @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do */ static get observedAttributes() { - const props = this._properties; - return props ? Object.keys(props).map(p => this.attributeNameForProperty(p)) : []; + if (!this.hasOwnProperty('__observedAttributes')) { + register(this.prototype); + const props = this._properties; + this.__observedAttributes = props ? Object.keys(props).map(p => this.attributeNameForProperty(p)) : []; + } + return this.__observedAttributes; } /** @@ -189,6 +194,7 @@ export const PropertiesMixin = dedupingMixin(superClass => { * @return {void} */ _initializeProperties() { + incrementInstanceCount(); this.constructor.finalize(); super._initializeProperties(); } diff --git a/lib/mixins/template-stamp.js b/lib/mixins/template-stamp.js index 3ae0607e01..0a36157c92 100644 --- a/lib/mixins/template-stamp.js +++ b/lib/mixins/template-stamp.js @@ -10,6 +10,9 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN import '../utils/boot.js'; import { dedupingMixin } from '../utils/mixin.js'; +import { legacyOptimizations } from '../utils/settings.js'; + +const walker = document.createTreeWalker(document); // 1.x backwards-compatible auto-wrapper for template type extensions // This is a clear layering violation and gives favored-nation status to @@ -45,7 +48,8 @@ function findTemplateNode(root, nodeInfo) { if (parent) { // note: marginally faster than indexing via childNodes // (http://jsperf.com/childnodes-lookup) - for (let n=parent.firstChild, i=0; n; n=n.nextSibling) { + walker.currentNode = parent; + for (let n=walker.firstChild(), i=0; n; n=walker.nextSibling()) { if (nodeInfo.parentIndex === i++) { return n; } @@ -200,7 +204,7 @@ export const TemplateStamp = dedupingMixin( if (!template._templateInfo) { let templateInfo = template._templateInfo = {}; templateInfo.nodeInfoList = []; - templateInfo.stripWhiteSpace = + templateInfo.stripWhiteSpace = legacyOptimizations || (outerTemplateInfo && outerTemplateInfo.stripWhiteSpace) || template.hasAttribute('strip-whitespace'); this._parseTemplateContent(template, templateInfo, {parent: null}); @@ -234,7 +238,8 @@ export const TemplateStamp = dedupingMixin( // For ShadyDom optimization, indicating there is an insertion point templateInfo.hasInsertionPoint = true; } - if (element.firstChild) { + walker.currentNode = element; + if (walker.firstChild()) { noted = this._parseTemplateChildNodes(element, templateInfo, nodeInfo) || noted; } if (element.hasAttributes && element.hasAttributes()) { @@ -260,7 +265,8 @@ export const TemplateStamp = dedupingMixin( if (root.localName === 'script' || root.localName === 'style') { return; } - for (let node=root.firstChild, parentIndex=0, next; node; node=next) { + walker.currentNode = root; + for (let node=walker.firstChild(), parentIndex=0, next; node; node=next) { // Wrap templates if (node.localName == 'template') { node = wrapTemplateExtension(node); @@ -269,12 +275,13 @@ export const TemplateStamp = dedupingMixin( // text nodes to be inexplicably split =( // note that root.normalize() should work but does not so we do this // manually. - next = node.nextSibling; + walker.currentNode = node; + next = walker.nextSibling(); if (node.nodeType === Node.TEXT_NODE) { let /** Node */ n = next; while (n && (n.nodeType === Node.TEXT_NODE)) { node.textContent += n.textContent; - next = n.nextSibling; + next = walker.nextSibling(); root.removeChild(n); n = next; } @@ -289,7 +296,8 @@ export const TemplateStamp = dedupingMixin( childInfo.infoIndex = templateInfo.nodeInfoList.push(/** @type {!NodeInfo} */(childInfo)) - 1; } // Increment if not removed - if (node.parentNode) { + walker.currentNode = node; + if (walker.parentNode()) { parentIndex++; } } diff --git a/lib/utils/settings.js b/lib/utils/settings.js index 5f4c0e443c..cdbcb8e36c 100644 --- a/lib/utils/settings.js +++ b/lib/utils/settings.js @@ -128,15 +128,16 @@ export const setAllowTemplateFromDomModule = function(allowDomModule) { * If no includes or relative urls are used in styles, these steps can be * skipped as an optimization. */ -export let skipStyleIncludesAndUrls = false; +export let legacyOptimizations = false; /** - * Sets `setSkipRewriteStyleUrls` globally for all elements + * Sets `legacyOptimizations` globally for all elements to enable optimizations + * when only legacy based elements are used. * - * @param {boolean} skipIncludesAndUrls enable or disable skipping style + * @param {boolean} useLegacyOptimizations enable or disable legacy optimizations * includes and url rewriting * @return {void} */ -export const setSkipStyleIncludesAndUrls = function(skipIncludesAndUrls) { - skipStyleIncludesAndUrls = skipIncludesAndUrls; +export const setLegacyOptimizations = function(useLegacyOptimizations) { + legacyOptimizations = useLegacyOptimizations; }; \ No newline at end of file diff --git a/lib/utils/telemetry.js b/lib/utils/telemetry.js new file mode 100644 index 0000000000..6668ac5271 --- /dev/null +++ b/lib/utils/telemetry.js @@ -0,0 +1,53 @@ +/** +@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 +*/ + +/** + * Total number of Polymer element instances created. + * @type {number} + */ +export let instanceCount = 0; + +export function incrementInstanceCount() { + instanceCount++; +} + +/** + * Array of Polymer element classes that have been finalized. + * @type {Array} + */ +export const registrations = []; + +/** + * @param {!PolymerElementConstructor} prototype Element prototype to log + * @this {this} + * @private + */ +function _regLog(prototype) { + console.log('[' + prototype.is + ']: registered'); +} + +/** + * Registers a class prototype for telemetry purposes. + * @param {HTMLElement} prototype Element prototype to register + * @this {this} + * @protected + */ +export function register(prototype) { + registrations.push(prototype); +} + +/** + * Logs all elements registered with an `is` to the console. + * @public + * @this {this} + */ +export function dumpRegistrations() { + registrations.forEach(_regLog); +} diff --git a/test/unit/disable-upgrade.html b/test/unit/disable-upgrade.html index 14dae2b60d..1c1fc78d7e 100644 --- a/test/unit/disable-upgrade.html +++ b/test/unit/disable-upgrade.html @@ -93,13 +93,17 @@

[[prop]]

type: String } }, + listeners: { + foo: 'fooHandler' + }, created() { this.hasCreated = true; this.prop = 'enabled!'; }, ready() { this.enabled = true; - } + }, + fooHandler() {} }); Polymer({ @@ -241,30 +245,39 @@

[[prop]]

assert.ok(el.$.enabledEl.enabled); assert.ok(el.$.enabledEl.$.element); assert.equal(el.$.enabledEl.$.element.textContent, 'enabled!'); + el.$.enabledEl.fooHandler = sinon.spy(); + el.$.enabledEl.fire('foo'); + assert.equal(el.$.enabledEl.fooHandler.callCount, 1); assert.notOk(el.$.disabledEl.hasCreated); assert.notOk(el.$.disabledEl.enabled); assert.notOk(el.$.disabledEl.$); + el.$.disabledEl.fooHandler = sinon.spy(); + el.$.disabledEl.fire('foo'); + assert.equal(el.$.disabledEl.fooHandler.callCount, 0); assert.notOk(el.$.disabledBoundEl.hasCreated); assert.notOk(el.$.disabledBoundEl.enabled); assert.notOk(el.$.disabledBoundEl.$); + el.$.disabledBoundEl.fooHandler = sinon.spy(); + el.$.disabledBoundEl.fire('foo'); + assert.equal(el.$.disabledBoundEl.fooHandler.callCount, 0); }); test('elements upgrade when `disable-upgrade` removed', function() { - assert.notOk(el.$.disabledEl.hasCreated); - assert.notOk(el.$.disabledEl.enabled); - assert.notOk(el.$.disabledEl.$); - assert.notOk(el.$.disabledBoundEl.hasCreated); - assert.notOk(el.$.disabledBoundEl.enabled); - assert.notOk(el.$.disabledBoundEl.$); el.enable(); assert.ok(el.$.disabledEl.hasCreated); assert.ok(el.$.disabledEl.enabled); assert.ok(el.$.disabledEl.$.element); assert.equal(el.$.disabledEl.$.element.textContent, 'enabled!'); + el.$.disabledEl.fooHandler = sinon.spy(); + el.$.disabledEl.fire('foo'); + assert.equal(el.$.disabledEl.fooHandler.callCount, 1); assert.ok(el.$.disabledBoundEl.hasCreated); assert.ok(el.$.disabledBoundEl.enabled); assert.ok(el.$.disabledBoundEl.$.element); assert.equal(el.$.disabledBoundEl.$.element.textContent, 'enabled!'); + el.$.disabledBoundEl.fooHandler = sinon.spy(); + el.$.disabledBoundEl.fire('foo'); + assert.equal(el.$.disabledBoundEl.fooHandler.callCount, 1); }); diff --git a/test/unit/mixin-behaviors.html b/test/unit/mixin-behaviors.html index 7c16962b20..8de238d2d0 100644 --- a/test/unit/mixin-behaviors.html +++ b/test/unit/mixin-behaviors.html @@ -256,12 +256,12 @@ import { mixinBehaviors } from '../../lib/legacy/class.js'; import { PolymerElement } from '../../polymer-element.js'; customElements.define('nested-behaviors', - class extends mixinBehaviors( - [ - [window.BehaviorB, [window.BehaviorC, window.BehaviorB], window.BehaviorA], - [window.BehaviorD] - ], PolymerElement) { -}); + class extends mixinBehaviors([window.BehaviorD, window.LifeCycleBehavior1], mixinBehaviors( + [ + [window.BehaviorB, [window.BehaviorC, window.BehaviorB], window.BehaviorA, window.LifeCycleBehavior2], + ], PolymerElement)) { + } +); @@ -355,7 +355,7 @@ @@ -554,7 +554,13 @@ }); test('nested-behavior dedups', function() { - assert.equal(el.behaviors.length, 4); + assert.equal(el.behaviors.length, 6); + }); + + test('nested-behavior lifecycle', function() { + assert.equal(el._calledCreated, 2, 'created call count wrong'); + assert.equal(el._calledAttached, 2, 'attached call count wrong'); + assert.equal(el._calledAttributeChanged, 1, 'attributeChanged call count wrong'); }); test('nested-behavior overrides ordering', function() {