| /** |
| @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 |
| */ |
| import '../utils/boot.js'; |
| |
| import { dedupingMixin } from '../utils/mixin.js'; |
| import { microTask } from '../utils/async.js'; |
| import { wrap } from '../utils/wrap.js'; |
| |
| /** @const {!AsyncInterface} */ |
| const microtask = microTask; |
| |
| /** |
| * Element class mixin that provides basic meta-programming for creating one |
| * or more property accessors (getter/setter pair) that enqueue an async |
| * (batched) `_propertiesChanged` callback. |
| * |
| * For basic usage of this mixin, call `MyClass.createProperties(props)` |
| * once at class definition time to create property accessors for properties |
| * named in props, implement `_propertiesChanged` to react as desired to |
| * property changes, and implement `static get observedAttributes()` and |
| * include lowercase versions of any property names that should be set from |
| * attributes. Last, call `this._enableProperties()` in the element's |
| * `connectedCallback` to enable the accessors. |
| * |
| * @mixinFunction |
| * @polymer |
| * @summary Element class mixin for reacting to property changes from |
| * generated property accessors. |
| */ |
| export const PropertiesChanged = dedupingMixin( |
| /** |
| * @template T |
| * @param {function(new:T)} superClass Class to apply mixin to. |
| * @return {function(new:T)} superClass with mixin applied. |
| */ |
| (superClass) => { |
| |
| /** |
| * @polymer |
| * @mixinClass |
| * @implements {Polymer_PropertiesChanged} |
| * @unrestricted |
| */ |
| class PropertiesChanged extends superClass { |
| |
| /** |
| * Creates property accessors for the given property names. |
| * @param {!Object} props Object whose keys are names of accessors. |
| * @return {void} |
| * @protected |
| */ |
| static createProperties(props) { |
| const proto = this.prototype; |
| for (let prop in props) { |
| // don't stomp an existing accessor |
| if (!(prop in proto)) { |
| proto._createPropertyAccessor(prop); |
| } |
| } |
| } |
| |
| /** |
| * Returns an attribute name that corresponds to the given property. |
| * The attribute name is the lowercased property name. Override to |
| * customize this mapping. |
| * @param {string} property Property to convert |
| * @return {string} Attribute name corresponding to the given property. |
| * |
| * @protected |
| */ |
| static attributeNameForProperty(property) { |
| return property.toLowerCase(); |
| } |
| |
| /** |
| * Override point to provide a type to which to deserialize a value to |
| * a given property. |
| * @param {string} name Name of property |
| * |
| * @protected |
| */ |
| static typeForProperty(name) { } //eslint-disable-line no-unused-vars |
| |
| /** |
| * Creates a setter/getter pair for the named property with its own |
| * local storage. The getter returns the value in the local storage, |
| * and the setter calls `_setProperty`, which updates the local storage |
| * for the property and enqueues a `_propertiesChanged` callback. |
| * |
| * This method may be called on a prototype or an instance. Calling |
| * this method may overwrite a property value that already exists on |
| * the prototype/instance by creating the accessor. |
| * |
| * @param {string} property Name of the property |
| * @param {boolean=} readOnly When true, no setter is created; the |
| * protected `_setProperty` function must be used to set the property |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _createPropertyAccessor(property, readOnly) { |
| this._addPropertyToAttributeMap(property); |
| if (!this.hasOwnProperty('__dataHasAccessor')) { |
| this.__dataHasAccessor = Object.assign({}, this.__dataHasAccessor); |
| } |
| if (!this.__dataHasAccessor[property]) { |
| this.__dataHasAccessor[property] = true; |
| this._definePropertyAccessor(property, readOnly); |
| } |
| } |
| |
| /** |
| * Adds the given `property` to a map matching attribute names |
| * to property names, using `attributeNameForProperty`. This map is |
| * used when deserializing attribute values to properties. |
| * |
| * @param {string} property Name of the property |
| * @override |
| */ |
| _addPropertyToAttributeMap(property) { |
| if (!this.hasOwnProperty('__dataAttributes')) { |
| this.__dataAttributes = Object.assign({}, this.__dataAttributes); |
| } |
| if (!this.__dataAttributes[property]) { |
| const attr = this.constructor.attributeNameForProperty(property); |
| this.__dataAttributes[attr] = property; |
| } |
| } |
| |
| /** |
| * Defines a property accessor for the given property. |
| * @param {string} property Name of the property |
| * @param {boolean=} readOnly When true, no setter is created |
| * @return {void} |
| * @override |
| */ |
| _definePropertyAccessor(property, readOnly) { |
| Object.defineProperty(this, property, { |
| /* eslint-disable valid-jsdoc */ |
| /** @this {PropertiesChanged} */ |
| get() { |
| return this._getProperty(property); |
| }, |
| /** @this {PropertiesChanged} */ |
| set: readOnly ? function () {} : function (value) { |
| this._setProperty(property, value); |
| } |
| /* eslint-enable */ |
| }); |
| } |
| |
| constructor() { |
| super(); |
| /** @protected {boolean} */ |
| this.__dataEnabled = false; |
| this.__dataReady = false; |
| this.__dataInvalid = false; |
| this.__data = {}; |
| this.__dataPending = null; |
| this.__dataOld = null; |
| this.__dataInstanceProps = null; |
| this.__serializing = false; |
| this._initializeProperties(); |
| } |
| |
| /** |
| * Lifecycle callback called when properties are enabled via |
| * `_enableProperties`. |
| * |
| * Users may override this function to implement behavior that is |
| * dependent on the element having its property data initialized, e.g. |
| * from defaults (initialized from `constructor`, `_initializeProperties`), |
| * `attributeChangedCallback`, or values propagated from host e.g. via |
| * bindings. `super.ready()` must be called to ensure the data system |
| * becomes enabled. |
| * |
| * @return {void} |
| * @public |
| * @override |
| */ |
| ready() { |
| this.__dataReady = true; |
| this._flushProperties(); |
| } |
| |
| /** |
| * Initializes the local storage for property accessors. |
| * |
| * Provided as an override point for performing any setup work prior |
| * to initializing the property accessor system. |
| * |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _initializeProperties() { |
| // Capture instance properties; these will be set into accessors |
| // during first flush. Don't set them here, since we want |
| // these to overwrite defaults/constructor assignments |
| for (let p in this.__dataHasAccessor) { |
| if (this.hasOwnProperty(p)) { |
| this.__dataInstanceProps = this.__dataInstanceProps || {}; |
| this.__dataInstanceProps[p] = this[p]; |
| delete this[p]; |
| } |
| } |
| } |
| |
| /** |
| * Called at ready time with bag of instance properties that overwrote |
| * accessors when the element upgraded. |
| * |
| * The default implementation sets these properties back into the |
| * setter at ready time. This method is provided as an override |
| * point for customizing or providing more efficient initialization. |
| * |
| * @param {Object} props Bag of property values that were overwritten |
| * when creating property accessors. |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _initializeInstanceProperties(props) { |
| Object.assign(this, props); |
| } |
| |
| /** |
| * Updates the local storage for a property (via `_setPendingProperty`) |
| * and enqueues a `_proeprtiesChanged` callback. |
| * |
| * @param {string} property Name of the property |
| * @param {*} value Value to set |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _setProperty(property, value) { |
| if (this._setPendingProperty(property, value)) { |
| this._invalidateProperties(); |
| } |
| } |
| |
| /** |
| * Returns the value for the given property. |
| * @param {string} property Name of property |
| * @return {*} Value for the given property |
| * @protected |
| * @override |
| */ |
| _getProperty(property) { |
| return this.__data[property]; |
| } |
| |
| /* eslint-disable no-unused-vars */ |
| /** |
| * Updates the local storage for a property, records the previous value, |
| * and adds it to the set of "pending changes" that will be passed to the |
| * `_propertiesChanged` callback. This method does not enqueue the |
| * `_propertiesChanged` callback. |
| * |
| * @param {string} property Name of the property |
| * @param {*} value Value to set |
| * @param {boolean=} ext Not used here; affordance for closure |
| * @return {boolean} Returns true if the property changed |
| * @protected |
| * @override |
| */ |
| _setPendingProperty(property, value, ext) { |
| let old = this.__data[property]; |
| let changed = this._shouldPropertyChange(property, value, old); |
| if (changed) { |
| if (!this.__dataPending) { |
| this.__dataPending = {}; |
| this.__dataOld = {}; |
| } |
| // Ensure old is captured from the last turn |
| if (this.__dataOld && !(property in this.__dataOld)) { |
| this.__dataOld[property] = old; |
| } |
| this.__data[property] = value; |
| this.__dataPending[property] = value; |
| } |
| return changed; |
| } |
| /* eslint-enable */ |
| |
| /** |
| * Marks the properties as invalid, and enqueues an async |
| * `_propertiesChanged` callback. |
| * |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _invalidateProperties() { |
| if (!this.__dataInvalid && this.__dataReady) { |
| this.__dataInvalid = true; |
| microtask.run(() => { |
| if (this.__dataInvalid) { |
| this.__dataInvalid = false; |
| this._flushProperties(); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Call to enable property accessor processing. Before this method is |
| * called accessor values will be set but side effects are |
| * queued. When called, any pending side effects occur immediately. |
| * For elements, generally `connectedCallback` is a normal spot to do so. |
| * It is safe to call this method multiple times as it only turns on |
| * property accessors once. |
| * |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _enableProperties() { |
| if (!this.__dataEnabled) { |
| this.__dataEnabled = true; |
| if (this.__dataInstanceProps) { |
| this._initializeInstanceProperties(this.__dataInstanceProps); |
| this.__dataInstanceProps = null; |
| } |
| this.ready(); |
| } |
| } |
| |
| /** |
| * Calls the `_propertiesChanged` callback with the current set of |
| * pending changes (and old values recorded when pending changes were |
| * set), and resets the pending set of changes. Generally, this method |
| * should not be called in user code. |
| * |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _flushProperties() { |
| const props = this.__data; |
| const changedProps = this.__dataPending; |
| const old = this.__dataOld; |
| if (this._shouldPropertiesChange(props, changedProps, old)) { |
| this.__dataPending = null; |
| this.__dataOld = null; |
| this._propertiesChanged(props, changedProps, old); |
| } |
| } |
| |
| /** |
| * Called in `_flushProperties` to determine if `_propertiesChanged` |
| * should be called. The default implementation returns true if |
| * properties are pending. Override to customize when |
| * `_propertiesChanged` is called. |
| * @param {!Object} currentProps Bag of all current accessor values |
| * @param {?Object} changedProps Bag of properties changed since the last |
| * call to `_propertiesChanged` |
| * @param {?Object} oldProps Bag of previous values for each property |
| * in `changedProps` |
| * @return {boolean} true if changedProps is truthy |
| * @override |
| */ |
| _shouldPropertiesChange(currentProps, changedProps, oldProps) { // eslint-disable-line no-unused-vars |
| return Boolean(changedProps); |
| } |
| |
| /** |
| * Callback called when any properties with accessors created via |
| * `_createPropertyAccessor` have been set. |
| * |
| * @param {!Object} currentProps Bag of all current accessor values |
| * @param {?Object} changedProps Bag of properties changed since the last |
| * call to `_propertiesChanged` |
| * @param {?Object} oldProps Bag of previous values for each property |
| * in `changedProps` |
| * @return {void} |
| * @protected |
| * @override |
| */ |
| _propertiesChanged(currentProps, changedProps, oldProps) { // eslint-disable-line no-unused-vars |
| } |
| |
| /** |
| * Method called to determine whether a property value should be |
| * considered as a change and cause the `_propertiesChanged` callback |
| * to be enqueued. |
| * |
| * The default implementation returns `true` if a strict equality |
| * check fails. The method always returns false for `NaN`. |
| * |
| * Override this method to e.g. provide stricter checking for |
| * Objects/Arrays when using immutable patterns. |
| * |
| * @param {string} property Property name |
| * @param {*} value New property value |
| * @param {*} old Previous property value |
| * @return {boolean} Whether the property should be considered a change |
| * and enqueue a `_proeprtiesChanged` callback |
| * @protected |
| * @override |
| */ |
| _shouldPropertyChange(property, value, old) { |
| return ( |
| // Strict equality check |
| (old !== value && |
| // This ensures (old==NaN, value==NaN) always returns false |
| (old === old || value === value)) |
| ); |
| } |
| |
| /** |
| * 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 |
| * @param {?string=} namespace Attribute namespace. |
| * @return {void} |
| * @suppress {missingProperties} Super may or may not implement the callback |
| * @override |
| */ |
| attributeChangedCallback(name, old, value, namespace) { |
| if (old !== value) { |
| this._attributeToProperty(name, value); |
| } |
| if (super.attributeChangedCallback) { |
| super.attributeChangedCallback(name, old, value, namespace); |
| } |
| } |
| |
| /** |
| * Deserializes an attribute to its associated property. |
| * |
| * This method calls the `_deserializeValue` method to convert the string to |
| * a typed value. |
| * |
| * @param {string} attribute Name of attribute to deserialize. |
| * @param {?string} value of the attribute. |
| * @param {*=} type type to deserialize to, defaults to the value |
| * returned from `typeForProperty` |
| * @return {void} |
| * @override |
| */ |
| _attributeToProperty(attribute, value, type) { |
| if (!this.__serializing) { |
| const map = this.__dataAttributes; |
| const property = map && map[attribute] || attribute; |
| this[property] = this._deserializeValue(value, type || |
| this.constructor.typeForProperty(property)); |
| } |
| } |
| |
| /** |
| * Serializes a property to its associated attribute. |
| * |
| * @suppress {invalidCasts} Closure can't figure out `this` is an element. |
| * |
| * @param {string} property Property name to reflect. |
| * @param {string=} attribute Attribute name to reflect to. |
| * @param {*=} value Property value to refect. |
| * @return {void} |
| * @override |
| */ |
| _propertyToAttribute(property, attribute, value) { |
| this.__serializing = true; |
| value = (arguments.length < 3) ? this[property] : value; |
| this._valueToNodeAttribute(/** @type {!HTMLElement} */(this), value, |
| attribute || this.constructor.attributeNameForProperty(property)); |
| this.__serializing = false; |
| } |
| |
| /** |
| * Sets a typed value to an HTML attribute on a node. |
| * |
| * This method calls the `_serializeValue` method to convert the typed |
| * value to a string. If the `_serializeValue` method returns `undefined`, |
| * the attribute will be removed (this is the default for boolean |
| * type `false`). |
| * |
| * @param {Element} node Element to set attribute to. |
| * @param {*} value Value to serialize. |
| * @param {string} attribute Attribute name to serialize to. |
| * @return {void} |
| * @override |
| */ |
| _valueToNodeAttribute(node, value, attribute) { |
| const str = this._serializeValue(value); |
| if (str === undefined) { |
| node.removeAttribute(attribute); |
| } else { |
| if (attribute === 'class' || attribute === 'name' || attribute === 'slot') { |
| node = /** @type {?Element} */(wrap(node)); |
| } |
| node.setAttribute(attribute, str); |
| } |
| } |
| |
| /** |
| * Converts a typed JavaScript value to a string. |
| * |
| * This method is called when setting JS property values to |
| * HTML attributes. Users may override this method to provide |
| * serialization for custom types. |
| * |
| * @param {*} value Property value to serialize. |
| * @return {string | undefined} String serialized from the provided |
| * property value. |
| * @override |
| */ |
| _serializeValue(value) { |
| switch (typeof value) { |
| case 'boolean': |
| return value ? '' : undefined; |
| default: |
| return value != null ? value.toString() : undefined; |
| } |
| } |
| |
| /** |
| * Converts a string to a typed JavaScript value. |
| * |
| * This method is called when reading HTML attribute values to |
| * JS properties. Users may override this method to provide |
| * deserialization for custom `type`s. Types for `Boolean`, `String`, |
| * and `Number` convert attributes to the expected types. |
| * |
| * @param {?string} value Value to deserialize. |
| * @param {*=} type Type to deserialize the string to. |
| * @return {*} Typed value deserialized from the provided string. |
| * @override |
| */ |
| _deserializeValue(value, type) { |
| switch (type) { |
| case Boolean: |
| return (value !== null); |
| case Number: |
| return Number(value); |
| default: |
| return value; |
| } |
| } |
| |
| } |
| |
| return PropertiesChanged; |
| }); |