mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
/**
|
|
@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 * as caseMap$0 from '../utils/case-map.js';
|
|
import { PropertiesChanged } from './properties-changed.js';
|
|
|
|
let caseMap = caseMap$0;
|
|
|
|
// Save map of native properties; this forms a blacklist or properties
|
|
// that won't have their values "saved" by `saveAccessorValue`, since
|
|
// reading from an HTMLElement accessor from the context of a prototype throws
|
|
const nativeProperties = {};
|
|
let proto = HTMLElement.prototype;
|
|
while (proto) {
|
|
let props = Object.getOwnPropertyNames(proto);
|
|
for (let i=0; i<props.length; i++) {
|
|
nativeProperties[props[i]] = true;
|
|
}
|
|
proto = Object.getPrototypeOf(proto);
|
|
}
|
|
|
|
/**
|
|
* Used to save the value of a property that will be overridden with
|
|
* an accessor. If the `model` is a prototype, the values will be saved
|
|
* in `__dataProto`, and it's up to the user (or downstream mixin) to
|
|
* decide how/when to set these values back into the accessors.
|
|
* If `model` is already an instance (it has a `__data` property), then
|
|
* the value will be set as a pending property, meaning the user should
|
|
* call `_invalidateProperties` or `_flushProperties` to take effect
|
|
*
|
|
* @param {Object} model Prototype or instance
|
|
* @param {string} property Name of property
|
|
* @return {void}
|
|
* @private
|
|
*/
|
|
function saveAccessorValue(model, property) {
|
|
// Don't read/store value for any native properties since they could throw
|
|
if (!nativeProperties[property]) {
|
|
let value = model[property];
|
|
if (value !== undefined) {
|
|
if (model.__data) {
|
|
// Adding accessor to instance; update the property
|
|
// It is the user's responsibility to call _flushProperties
|
|
model._setPendingProperty(property, value);
|
|
} else {
|
|
// Adding accessor to proto; save proto's value for instance-time use
|
|
if (!model.__dataProto) {
|
|
model.__dataProto = {};
|
|
} else if (!model.hasOwnProperty(JSCompiler_renameProperty('__dataProto', model))) {
|
|
model.__dataProto = Object.create(model.__dataProto);
|
|
}
|
|
model.__dataProto[property] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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:
|
|
*
|
|
* - Declare attributes to observe via the standard `static get observedAttributes()`. Use
|
|
* `dash-case` attribute names to represent `camelCase` property names.
|
|
* - Implement the `_propertiesChanged` callback on the class.
|
|
* - Call `MyClass.createPropertiesForAttributes()` **once** on the class to generate
|
|
* property accessors for each observed attribute. This must be called before the first
|
|
* instance is created, for example, by calling it before calling `customElements.define`.
|
|
* It can also be called lazily from the element's `constructor`, as long as it's guarded so
|
|
* that the call is only made once, when the first instance is created.
|
|
* - Call `this._enableProperties()` in the element's `connectedCallback` to enable
|
|
* the accessors.
|
|
*
|
|
* Any `observedAttributes` will automatically be
|
|
* deserialized via `attributeChangedCallback` and set to the associated
|
|
* property using `dash-case`-to-`camelCase` convention.
|
|
*
|
|
* @mixinFunction
|
|
* @polymer
|
|
* @appliesMixin PropertiesChanged
|
|
* @summary Element class mixin for reacting to property changes from
|
|
* generated property accessors.
|
|
*/
|
|
export const PropertyAccessors = dedupingMixin(superClass => {
|
|
|
|
/**
|
|
* @constructor
|
|
* @extends {superClass}
|
|
* @implements {Polymer_PropertiesChanged}
|
|
* @unrestricted
|
|
* @private
|
|
*/
|
|
const base = PropertiesChanged(superClass);
|
|
|
|
/**
|
|
* @polymer
|
|
* @mixinClass
|
|
* @implements {Polymer_PropertyAccessors}
|
|
* @extends {base}
|
|
* @unrestricted
|
|
*/
|
|
class PropertyAccessors extends base {
|
|
|
|
/**
|
|
* Generates property accessors for all attributes in the standard
|
|
* static `observedAttributes` array.
|
|
*
|
|
* Attribute names are mapped to property names using the `dash-case` to
|
|
* `camelCase` convention
|
|
*
|
|
* @return {void}
|
|
*/
|
|
static createPropertiesForAttributes() {
|
|
let a$ = this.observedAttributes;
|
|
for (let i=0; i < a$.length; i++) {
|
|
this.prototype._createPropertyAccessor(caseMap.dashToCamelCase(a$[i]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an attribute name that corresponds to the given property.
|
|
* By default, converts camel to dash case, e.g. `fooBar` to `foo-bar`.
|
|
* @param {string} property Property to convert
|
|
* @return {string} Attribute name corresponding to the given property.
|
|
*
|
|
* @protected
|
|
*/
|
|
static attributeNameForProperty(property) {
|
|
return caseMap.camelToDashCase(property);
|
|
}
|
|
|
|
/**
|
|
* Overrides PropertiesChanged implementation to initialize values for
|
|
* accessors created for values that already existed on the element
|
|
* prototype.
|
|
*
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_initializeProperties() {
|
|
if (this.__dataProto) {
|
|
this._initializeProtoProperties(this.__dataProto);
|
|
this.__dataProto = null;
|
|
}
|
|
super._initializeProperties();
|
|
}
|
|
|
|
/**
|
|
* Called at instance time with bag of properties that were overwritten
|
|
* by accessors on the prototype when accessors were created.
|
|
*
|
|
* The default implementation sets these properties back into the
|
|
* setter at instance 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
|
|
*/
|
|
_initializeProtoProperties(props) {
|
|
for (let p in props) {
|
|
this._setProperty(p, props[p]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures the element has the given attribute. If it does not,
|
|
* assigns the given value to the attribute.
|
|
*
|
|
* @suppress {invalidCasts} Closure can't figure out `this` is infact an element
|
|
*
|
|
* @param {string} attribute Name of attribute to ensure is set.
|
|
* @param {string} value of the attribute.
|
|
* @return {void}
|
|
*/
|
|
_ensureAttribute(attribute, value) {
|
|
const el = /** @type {!HTMLElement} */(this);
|
|
if (!el.hasAttribute(attribute)) {
|
|
this._valueToNodeAttribute(el, value, attribute);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides PropertiesChanged implemention to serialize objects as JSON.
|
|
*
|
|
* @param {*} value Property value to serialize.
|
|
* @return {string | undefined} String serialized from the provided property value.
|
|
*/
|
|
_serializeValue(value) {
|
|
/* eslint-disable no-fallthrough */
|
|
switch (typeof value) {
|
|
case 'object':
|
|
if (value instanceof Date) {
|
|
return value.toString();
|
|
} else if (value) {
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch(x) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
default:
|
|
return super._serializeValue(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a string to a typed JavaScript value.
|
|
*
|
|
* This method is called by Polymer when reading HTML attribute values to
|
|
* JS properties. Users may override this method on Polymer element
|
|
* prototypes to provide deserialization for custom `type`s. Note,
|
|
* the `type` argument is the value of the `type` field provided in the
|
|
* `properties` configuration object for a given property, and is
|
|
* by convention the constructor for the type to deserialize.
|
|
*
|
|
*
|
|
* @param {?string} value Attribute value to deserialize.
|
|
* @param {*=} type Type to deserialize the string to.
|
|
* @return {*} Typed value deserialized from the provided string.
|
|
*/
|
|
_deserializeValue(value, type) {
|
|
/**
|
|
* @type {*}
|
|
*/
|
|
let outValue;
|
|
switch (type) {
|
|
case Object:
|
|
try {
|
|
outValue = JSON.parse(/** @type {string} */(value));
|
|
} catch(x) {
|
|
// allow non-JSON literals like Strings and Numbers
|
|
outValue = value;
|
|
}
|
|
break;
|
|
case Array:
|
|
try {
|
|
outValue = JSON.parse(/** @type {string} */(value));
|
|
} catch(x) {
|
|
outValue = null;
|
|
console.warn(`Polymer::Attributes: couldn't decode Array as JSON: ${value}`);
|
|
}
|
|
break;
|
|
case Date:
|
|
outValue = isNaN(value) ? String(value) : Number(value);
|
|
outValue = new Date(outValue);
|
|
break;
|
|
default:
|
|
outValue = super._deserializeValue(value, type);
|
|
break;
|
|
}
|
|
return outValue;
|
|
}
|
|
/* eslint-enable no-fallthrough */
|
|
|
|
/**
|
|
* Overrides PropertiesChanged implementation to save existing prototype
|
|
* property value so that it can be reset.
|
|
* @param {string} property Name of the property
|
|
* @param {boolean=} readOnly When true, no setter is created
|
|
*
|
|
* When calling on a prototype, any overwritten values are saved in
|
|
* `__dataProto`, and it is up to the subclasser to decide how/when
|
|
* to set those properties back into the accessor. When calling on an
|
|
* instance, the overwritten value is set via `_setPendingProperty`,
|
|
* and the user should call `_invalidateProperties` or `_flushProperties`
|
|
* for the values to take effect.
|
|
* @protected
|
|
* @return {void}
|
|
*/
|
|
_definePropertyAccessor(property, readOnly) {
|
|
saveAccessorValue(this, property);
|
|
super._definePropertyAccessor(property, readOnly);
|
|
}
|
|
|
|
/**
|
|
* Returns true if this library created an accessor for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {boolean} True if an accessor was created
|
|
*/
|
|
_hasAccessor(property) {
|
|
return this.__dataHasAccessor && this.__dataHasAccessor[property];
|
|
}
|
|
|
|
/**
|
|
* Returns true if the specified property has a pending change.
|
|
*
|
|
* @param {string} prop Property name
|
|
* @return {boolean} True if property has a pending change
|
|
* @protected
|
|
*/
|
|
_isPropertyPending(prop) {
|
|
return Boolean(this.__dataPending && (prop in this.__dataPending));
|
|
}
|
|
|
|
}
|
|
|
|
return PropertyAccessors;
|
|
|
|
});
|