diff --git a/lib/mixins/properties-mixin.html b/lib/mixins/properties-mixin.html index 298ac04e57..d0d76ee375 100644 --- a/lib/mixins/properties-mixin.html +++ b/lib/mixins/properties-mixin.html @@ -46,6 +46,41 @@ return value !== null; } + /** + * Mixes `moreProps` into `props` but upgrades shorthand type + * syntax to { type: Type}. + * + * @param {Object} props Bag to collect flattened properties into + * @param {Object} moreProps Bag of properties to add to `props` + * @return {Object} The input `props` bag + * @private + */ + function mixProperties(props, moreProps) { + for (let p in moreProps) { + let o = moreProps[p]; + if (typeof o == 'function') { + o = { type: o }; + } + props[p] = o; + } + return props; + } + + /** + * Returns the super class constructor for the given class, if it is an + * instance of the PropertiesClass. + * + * @param {PropertiesClassConstructor} ctor PropertiesClass constructor + * @returns {PropertiesClassConstructor} Super class constructor + */ + function superForClass(ctor) { + const proto = /** @type {PropertiesClassConstructor} */ (ctor).prototype; + const superCtor = Object.getPrototypeOf(proto).constructor; + if (superCtor.prototype instanceof PropertiesClass) { + return superCtor; + } + } + /** * @polymer * @mixinClass @@ -53,7 +88,7 @@ * @implements {Polymer_PropertiesMixin} * @unrestricted */ - class Properties extends base { + class PropertiesClass extends base { static get BooleanAttribute() { return BooleanAttribute; @@ -64,75 +99,96 @@ * listed in `properties`. */ static get observedAttributes() { - this._ensurePropertyInfo(); - const props = this.prototype.__propertyInfo; + const props = this._properties; return props ? Object.keys(props).map(p => { return this.prototype._attributeForProperty(p); }) : []; } - _propertyForAttribute(name) { - return this.__attributeInfo[name] || name; + /** + * Finalizes an element definition, including ensuring any super classes + * are also finalized. This includes ensuring property + * accessors exist on the element prototype. This method calls + * `_finalizeClass` to finalize each constructor in the prototype chain. + * @param {string} name Name of the element + */ + static finalize() { // eslint-disable-line no-unused-vars + if (!this.hasOwnProperty(JSCompiler_renameProperty('__finalized', this))) { + const superCtor = superForClass(this); + if (superCtor) { + superCtor.finalize(); + } + this.__finalized = true; + this._finalizeClass(); + } } - _attributeForProperty(name) { - const info = this.__propertyInfo[name]; - return info && info.attribute || name.toLowerCase(); + /** + * Finalize an element class. This includes ensuring property + * accessors exist on the element prototype. This method is called by + * `finalize` and finalizes the class constructor. + * + * @protected + */ + static _finalizeClass() { + const props = this._ownProperties; + if (props) { + this.createProperties(props); + } } - static _ensureFinalized(name) { - if (!this.prototype.hasOwnProperty('__finalized')) { - this.finalize(name); + /** + * Returns a memoized version of the `properties` object. Properties + * not in object format are converted to at lesat {type}. + * + * @return {Object} Object containing own properties for this class + * @protected + */ + static get _ownProperties() { + if (!this.hasOwnProperty(JSCompiler_renameProperty('__ownProperties', this))) { + const props = this.properties; + this.__ownProperties = props ? mixProperties({}, props) : null; } + return this.__ownProperties; } /** - * Finalizes an element definition. This includes ensuring property - * accessors exist on the element prototype and parsing the element - * template. - * @param {string} name Name of the element + * Returns a memoized version of all properties, including those inherited + * from super classes. Properties not in object format are converted to + * at lesat {type}. + * + * @return {Object} Object containing properties for this class + * @protected */ - static finalize(name) { // eslint-disable-line no-unused-vars - this.prototype.__finalized = true; - this._ensurePropertyInfo(); - const props = this.prototype.__propertyInfo; - if (props) { - this.createProperties(Object.keys(props)); + static get _properties() { + if (!this.hasOwnProperty(JSCompiler_renameProperty('__properties', this))) { + const superCtor = superForClass(this); + this.__properties = Object.assign({}, + superCtor && superCtor._properties, this._ownProperties); } + return this.__properties; } - static _ensurePropertyInfo() { - let proto = this.prototype; - if (!proto.hasOwnProperty('__propertyInfo')) { - const props = this.prototype.__propertyInfo = this.properties; - const attrInfo = this.prototype.__attributeInfo = {}; + static get _attributeInfo() { + if (!this.hasOwnProperty(JSCompiler_renameProperty('__attributeInfo', this))) { + const proto = this.prototype; + const attrInfo = this.__attributeInfo = {}; + const props = this._properties; for (let prop in props) { attrInfo[proto._attributeForProperty(prop)] = prop; } } + return this.__attributeInfo; } - /** - * Overrides implementation in PropertiesChanged to immediately process - * any pending changes to properties and ensure that - * `_propertiesChanged` is called. - * - * @public - */ - ready() { - super.ready(); - this._invalidateProperties(); - this._validateProperties(); + _propertyForAttribute(name) { + return this.constructor._attributeInfo[name] || + super._propertyForAttribute(name); } - /** - * Overrides default behavior and adds a call to `finalize` which lazily - * configures the element's property accessors. - * @override - */ - _initializeProperties() { - this.constructor._ensureFinalized(this.localName); - super._initializeProperties(); + _attributeForProperty(name) { + const info = this.constructor._properties[name]; + return info && info.attribute || super._attributeForProperty(name); } /** @@ -144,10 +200,20 @@ * @protected */ _typeForProperty(name) { - const info = this.__propertyInfo[name]; + const info = this.constructor._properties[name]; return info.type || info; } + /** + * Overrides default behavior and adds a call to `finalize` which lazily + * configures the element's property accessors. + * @override + */ + _initializeProperties() { + this.constructor.finalize(this.localName); + super._initializeProperties(); + } + /** * Called when the element is added to a document. * Calls `_enableProperties` to turn on property system from @@ -164,7 +230,7 @@ } - return Properties; + return PropertiesClass; }); diff --git a/test/unit/polymer.properties-element-with-property-accessors.html b/test/unit/polymer.properties-element-with-property-accessors.html index f72042bdcf..7d55b9ec53 100644 --- a/test/unit/polymer.properties-element-with-property-accessors.html +++ b/test/unit/polymer.properties-element-with-property-accessors.html @@ -77,9 +77,9 @@ class SubElement extends window.MyElement { static get properties() { - return Object.assign({ + return { prop2: String - }, super.properties); + }; } constructor() { @@ -118,9 +118,9 @@ return class extends Base { static get properties() { - return Object.assign({ + return { mixin: Number - }, super.properties); + }; } constructor() { @@ -147,10 +147,10 @@ class SubMixinElement extends MyMixin(window.SubElement) { static get properties() { - return Object.assign({ + return { prop3: String, camelCase: String - }, super.properties); + }; } constructor() { diff --git a/test/unit/polymer.properties-element.html b/test/unit/polymer.properties-element.html index d3cc759b96..f73b36f4ca 100644 --- a/test/unit/polymer.properties-element.html +++ b/test/unit/polymer.properties-element.html @@ -75,9 +75,9 @@ class SubElement extends window.MyElement { static get properties() { - return Object.assign({ + return { prop2: String - }, super.properties); + } } constructor() { @@ -115,9 +115,9 @@ return class extends Base { static get properties() { - return Object.assign({ + return { mixin: Number - }, super.properties); + }; } constructor() { @@ -143,10 +143,10 @@ class SubMixinElement extends MyMixin(window.SubElement) { static get properties() { - return Object.assign({ + return { prop3: String, camelCase: String - }, super.properties); + }; } constructor() { @@ -233,7 +233,7 @@ }); test('attributes', function() { - var fixtureEl = fixture('my-element-attr'); + const fixtureEl = fixture('my-element-attr'); assert.equal(fixtureEl.prop, 'attr'); assert.equal(fixtureEl._callAttributeChangedCallback, 1); }); @@ -242,7 +242,7 @@ suite('subclass', function() { - var el; + let el; setup(function() { el = document.createElement('sub-element');