535 lines
20 KiB
HTML
535 lines
20 KiB
HTML
<!--
|
|
@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
|
|
-->
|
|
|
|
<link rel="import" href="../utils/boot.html">
|
|
<link rel="import" href="../utils/mixin.html">
|
|
<link rel="import" href="../utils/async.html">
|
|
|
|
<script>
|
|
(function () {
|
|
|
|
'use strict';
|
|
|
|
/** @const {!AsyncInterface} */
|
|
const microtask = Polymer.Async.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
|
|
* @memberof Polymer
|
|
* @summary Element class mixin for reacting to property changes from
|
|
* generated property accessors.
|
|
*/
|
|
Polymer.PropertiesChanged = Polymer.dedupingMixin(superClass => {
|
|
|
|
/**
|
|
* @polymer
|
|
* @mixinClass
|
|
* @extends {superClass}
|
|
* @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
|
|
*/
|
|
_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
|
|
*/
|
|
_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}
|
|
*/
|
|
_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();
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
*/
|
|
_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
|
|
* @return {void}
|
|
* @suppress {missingProperties} Super may or may not implement the callback
|
|
*/
|
|
attributeChangedCallback(name, old, value) {
|
|
if (old !== value) {
|
|
this._attributeToProperty(name, value);
|
|
}
|
|
if (super.attributeChangedCallback) {
|
|
super.attributeChangedCallback(name, old, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
_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}
|
|
*/
|
|
_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}
|
|
*/
|
|
_valueToNodeAttribute(node, value, attribute) {
|
|
const str = this._serializeValue(value);
|
|
if (str === undefined) {
|
|
node.removeAttribute(attribute);
|
|
} else {
|
|
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.
|
|
*/
|
|
_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.
|
|
*/
|
|
_deserializeValue(value, type) {
|
|
switch (type) {
|
|
case Boolean:
|
|
return (value !== null);
|
|
case Number:
|
|
return Number(value);
|
|
default:
|
|
return value;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return PropertiesChanged;
|
|
});
|
|
|
|
|
|
})();
|
|
|
|
</script>
|