diff --git a/lib/legacy/class.js b/lib/legacy/class.js index 9db14be0be..dc03f8e15d 100644 --- a/lib/legacy/class.js +++ b/lib/legacy/class.js @@ -9,8 +9,11 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN */ import { LegacyElementMixin } from './legacy-element-mixin.js'; -import { legacyOptimizations } from '../utils/settings.js'; -import { DisableUpgradeMixin } from '../mixins/disable-upgrade-mixin.js'; +import { legacyOptimizations, legacyNoObservedAttributes } from '../utils/settings.js'; +import { wrap } from '../utils/wrap.js'; + +const DISABLED_ATTR = 'disable-upgrade'; +let observedAttributesGetter; const lifecycleProps = { attached: true, @@ -208,6 +211,22 @@ function mergeProperties(target, source) { */ function GenerateClassFromInfo(info, Base, behaviors) { + // Work around for closure bug #126934458. Using `super` in a property + // getter does not work so instead we search the Base prototype for an + // implementation of observedAttributes so that we can override and call + // the `super` getter. Note, this is done one time ever because we assume + // that `Base` is always comes from `Polymer.LegacyElementMixn`. + if (!observedAttributesGetter) { + let ctor = Base; + while (ctor && !observedAttributesGetter) { + const desc = Object.getOwnPropertyDescriptor(ctor, 'observedAttributes'); + if (desc) { + observedAttributesGetter = desc.get; + } + ctor = Object.getPrototypeOf(ctor.prototype).constructor; + } + } + // manages behavior and lifecycle processing (filled in after class definition) let behaviorList; const lifecycle = {}; @@ -280,6 +299,14 @@ function GenerateClassFromInfo(info, Base, behaviors) { * @return {void} */ created() { + // Pull all attribute values 1x if `legacyNoObservedAttributes` is set. + if (legacyNoObservedAttributes && this.hasAttributes()) { + const a = this.attributes; + for (let i=0, l=a.length; i < l; i++) { + const attr = a[i]; + this.__attributeReaction(attr.name, undefined, attr.value) + } + } super.created(); const list = lifecycle.created; if (list) { @@ -289,6 +316,32 @@ function GenerateClassFromInfo(info, Base, behaviors) { } } + __attributeReaction(name, oldValue, value) { + if ((this.__dataAttributes && this.__dataAttributes[name]) || name === DISABLED_ATTR) { + this.attributeChangedCallback(name, oldValue, value); + } + } + + setAttribute(name, value) { + if (legacyNoObservedAttributes) { + const oldValue = this.getAttribute(name); + super.setAttribute(name, value); + this.__attributeReaction(name, oldValue, value); + } else { + super.setAttribute(name, value); + } + } + + removeAttribute(name) { + if (legacyNoObservedAttributes) { + const oldValue = this.getAttribute(name); + super.removeAttribute(name); + this.__attributeReaction(name, oldValue, null); + } else { + super.removeAttribute(name); + } + } + /** * @return {void} */ @@ -423,6 +476,95 @@ function GenerateClassFromInfo(info, Base, behaviors) { } } } + + // NOTE: Below is an inlined version of DisableUpgradeMixin. It is inlined + // as a performance optimization to avoid the need to place the mixin on + // top of every legacy element. + constructor() { + super(); + /** @type {boolean|undefined} */ + this.__isUpgradeDisabled; + } + + static get observedAttributes() { + return legacyNoObservedAttributes ? [] : + observedAttributesGetter.call(this).concat(DISABLED_ATTR); + } + + // Prevent element from initializing properties when it's upgrade disabled. + /** @override */ + _initializeProperties() { + if (this.hasAttribute(DISABLED_ATTR)) { + this.__isUpgradeDisabled = true; + } else { + super._initializeProperties(); + } + } + + // Prevent element from enabling properties when it's upgrade disabled. + // Normally overriding connectedCallback would be enough, but dom-* elements + /** @override */ + _enableProperties() { + if (!this.__isUpgradeDisabled) { + super._enableProperties(); + } + } + + // If the element starts upgrade-disabled and a property is set for + // which an accessor exists, the default should not be applied. + // This additional check is needed because defaults are applied via + // `_initializeProperties` which is called after initial properties + // have been set when the element starts upgrade-disabled. + /** @override */ + _canApplyPropertyDefault(property) { + return super._canApplyPropertyDefault(property) && + !(this.__isUpgradeDisabled && this._isPropertyPending(property)); + } + + /** + * @override + * @param {string} name Attribute name. + * @param {?string} old The previous value for the attribute. + * @param {?string} value The new value for the attribute. + * @param {?string} namespace The XML namespace for the attribute. + * @return {void} + */ + attributeChangedCallback(name, old, value, namespace) { + if (name == DISABLED_ATTR) { + // When disable-upgrade is removed, intialize properties and + // provoke connectedCallback if the element is already connected. + if (this.__isUpgradeDisabled && value == null) { + super._initializeProperties(); + this.__isUpgradeDisabled = false; + if (wrap(this).isConnected) { + super.connectedCallback(); + } + } + } else { + super.attributeChangedCallback( + name, old, value, /** @type {null|string} */ (namespace)); + } + } + + // Prevent element from connecting when it's upgrade disabled. + // This prevents user code in `attached` from being called. + /** @override */ + connectedCallback() { + if (!this.__isUpgradeDisabled) { + super.connectedCallback(); + } + } + + // Prevent element from disconnecting when it's upgrade disabled. + // This avoids allowing user code `detached` from being called without a + // paired call to `attached`. + /** @override */ + disconnectedCallback() { + if (!this.__isUpgradeDisabled) { + super.disconnectedCallback(); + } + } + } // apply behaviors, note actual copying is done lazily at first instance creation @@ -457,6 +599,8 @@ function GenerateClassFromInfo(info, Base, behaviors) { return PolymerGenerated; } +const LegacyElement = LegacyElementMixin(HTMLElement); + /** * Generates a class that extends `LegacyElement` based on the * provided info object. Metadata objects on the `info` object @@ -531,13 +675,10 @@ export const Class = function(info, mixin) { if (!info) { console.warn('Polymer.Class requires `info` argument'); } - let klass = mixin ? mixin(LegacyElementMixin(HTMLElement)) : - LegacyElementMixin(HTMLElement); + let klass = mixin ? mixin(LegacyElement) : + LegacyElement; klass = GenerateClassFromInfo(info, klass, info.behaviors); // decorate klass with registration info klass.is = klass.prototype.is = info.is; - if (legacyOptimizations) { - klass = DisableUpgradeMixin(klass); - } return klass; }; diff --git a/lib/utils/mixin.js b/lib/utils/mixin.js index b79c7f9a20..156175f61a 100644 --- a/lib/utils/mixin.js +++ b/lib/utils/mixin.js @@ -52,13 +52,13 @@ export const dedupingMixin = function(mixin) { if (!extended) { extended = /** @type {!Function} */(mixin)(base); map.set(base, extended); + // copy inherited mixin set from the extended class, or the base class + // NOTE: we avoid use of Set here because some browser (IE11) + // cannot extend a base Set via the constructor. + let mixinSet = Object.create(/** @type {!MixinFunction} */(extended).__mixinSet || baseSet || null); + mixinSet[mixinDedupeId] = true; + /** @type {!MixinFunction} */(extended).__mixinSet = mixinSet; } - // copy inherited mixin set from the extended class, or the base class - // NOTE: we avoid use of Set here because some browser (IE11) - // cannot extend a base Set via the constructor. - let mixinSet = Object.create(/** @type {!MixinFunction} */(extended).__mixinSet || baseSet || null); - mixinSet[mixinDedupeId] = true; - /** @type {!MixinFunction} */(extended).__mixinSet = mixinSet; return extended; } diff --git a/lib/utils/settings.js b/lib/utils/settings.js index 6685be2be3..13d7634eaf 100644 --- a/lib/utils/settings.js +++ b/lib/utils/settings.js @@ -312,3 +312,21 @@ export let suppressTemplateNotifications = export const setSuppressTemplateNotifications = function(suppress) { suppressTemplateNotifications = suppress; }; + +/** + * Setting to disable use of dynamic attributes. This is an optimization + * to avoid setting `observedAttributes`. Instead attributes are read + * once at create time. + */ +export let legacyNoObservedAttributes = + window.Polymer && window.Polymer.legacyNoObservedAttributes || false; + +/** + * Sets `legacyNoObservedAttributes` globally, to disable `observedAttributes`. + * + * @param {boolean} disables `observedAttributes` + * @return {void} + */ +export const setLegacyNoObservedAttributes = function(noAttributes) { + legacyNoObservedAttributes = noAttributes; +}; \ No newline at end of file diff --git a/test/unit/legacy-noattributes.html b/test/unit/legacy-noattributes.html new file mode 100644 index 0000000000..848769c798 --- /dev/null +++ b/test/unit/legacy-noattributes.html @@ -0,0 +1,150 @@ + + + +
+ + + + + + + + +