UNPKG

@polymer/polymer

Version:

The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to

593 lines (560 loc) 20.5 kB
/** @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. * @template T * @param {function(new:T)} superClass Class to apply mixin to. * @return {function(new:T)} superClass with mixin applied. */ 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 * @nocollapse */ 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 * @nocollapse */ 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 * @nocollapse */ 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(JSCompiler_renameProperty('__dataHasAccessor', this))) { 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(JSCompiler_renameProperty('__dataAttributes', this))) { this.__dataAttributes = Object.assign({}, this.__dataAttributes); } // This check is technically not correct; it's an optimization that // assumes that if a _property_ name is already in the map (note this is // an attr->property map), the property mapped directly to the attribute // and it has already been mapped. This would fail if // `attributeNameForProperty` were overridden such that this was not the // case. let attr = this.__dataAttributes[property]; if (!attr) { attr = this.constructor.attributeNameForProperty(property); this.__dataAttributes[attr] = property; } return attr; } /** * 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() { // Inline for perf instead of using `_getProperty` return this.__data[property]; }, /** @this {PropertiesChanged} */ set: readOnly ? function () {} : function (value) { // Inline for perf instead of using `_setProperty` if (this._setPendingProperty(property, value, true)) { this._invalidateProperties(); } } /* eslint-enable */ }); } constructor() { super(); /** @type {boolean} */ this.__dataEnabled = false; this.__dataReady = false; this.__dataInvalid = false; this.__data = {}; this.__dataPending = null; this.__dataOld = null; this.__dataInstanceProps = null; /** @type {number} */ // NOTE: used to track re-entrant calls to `_flushProperties` this.__dataCounter = 0; 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 */ /** * @param {string} property Name of the property * @return {boolean} Returns true if the property is pending. */ _isPropertyPending(property) { return !!(this.__dataPending && this.__dataPending.hasOwnProperty(property)); } /** * 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() { this.__dataCounter++; 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); } this.__dataCounter--; } /** * 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 (attribute === 'class' || attribute === 'name' || attribute === 'slot') { node = /** @type {?Element} */(wrap(node)); } if (str === undefined) { node.removeAttribute(attribute); } else { node.setAttribute( attribute, // Closure's type for `setAttribute`'s second parameter incorrectly // excludes `TrustedScript`. (str === '' && window.trustedTypes) ? /** @type {?} */ (window.trustedTypes.emptyScript) : 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; });