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

345 lines (323 loc) 11.8 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 { camelToDashCase, dashToCamelCase } from '../utils/case-map.js'; import { PropertiesChanged } from './properties-changed.js'; // 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); } const isTrustedType = (() => { if (!window.trustedTypes) { return () => false; } return (val) => trustedTypes.isHTML(val) || trustedTypes.isScript(val) || trustedTypes.isScriptURL(val); })(); /** * 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. * @template T * @param {function(new:T)} superClass Class to apply mixin to. * @return {function(new:T)} superClass with mixin applied. */ export const PropertyAccessors = dedupingMixin(superClass => { /** * @constructor * @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} * @nocollapse */ static createPropertiesForAttributes() { let a$ = /** @type {?} */ (this).observedAttributes; for (let i=0; i < a$.length; i++) { this.prototype._createPropertyAccessor(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 * @nocollapse */ static attributeNameForProperty(property) { return camelToDashCase(property); } /** * Overrides PropertiesChanged implementation to initialize values for * accessors created for values that already existed on the element * prototype. * * @return {void} * @protected * @override */ _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 * @override */ _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} * @override */ _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. * @override */ _serializeValue(value) { /* eslint-disable no-fallthrough */ switch (typeof value) { case 'object': if (value instanceof Date) { return value.toString(); } else if (value) { if (isTrustedType(value)) { /** * Here `value` isn't actually a string, but it should be * passed into APIs that normally expect a string, like * elem.setAttribute. */ return /** @type {?} */ (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. * @override */ _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} * @override */ _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 * @override */ _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 * @override */ _isPropertyPending(prop) { return Boolean(this.__dataPending && (prop in this.__dataPending)); } } return PropertyAccessors; });