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

756 lines (720 loc) 31.6 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 --> <link rel="import" href="../utils/boot.html"> <link rel="import" href="../utils/settings.html"> <link rel="import" href="../utils/mixin.html"> <link rel="import" href="../utils/style-gather.html"> <link rel="import" href="../utils/resolve-url.html"> <link rel="import" href="../elements/dom-module.html"> <link rel="import" href="property-effects.html"> <link rel="import" href="properties-mixin.html"> <script> (function() { 'use strict'; const builtCSS = window.ShadyCSS && window.ShadyCSS['cssBuild']; /** * Element class mixin that provides the core API for Polymer's meta-programming * features including template stamping, data-binding, attribute deserialization, * and property change observation. * * Subclassers may provide the following static getters to return metadata * used to configure Polymer's features for the class: * * - `static get is()`: When the template is provided via a `dom-module`, * users should return the `dom-module` id from a static `is` getter. If * no template is needed or the template is provided directly via the * `template` getter, there is no need to define `is` for the element. * * - `static get template()`: Users may provide the template directly (as * opposed to via `dom-module`) by implementing a static `template` getter. * The getter may return an `HTMLTemplateElement` or a string, which will * automatically be parsed into a template. * * - `static get properties()`: Should return an object describing * property-related metadata used by Polymer features (key: property name * value: object containing property metadata). Valid keys in per-property * metadata include: * - `type` (String|Number|Object|Array|...): Used by * `attributeChangedCallback` to determine how string-based attributes * are deserialized to JavaScript property values. * - `notify` (boolean): Causes a change in the property to fire a * non-bubbling event called `<property>-changed`. Elements that have * enabled two-way binding to the property use this event to observe changes. * - `readOnly` (boolean): Creates a getter for the property, but no setter. * To set a read-only property, use the private setter method * `_setProperty(property, value)`. * - `observer` (string): Observer method name that will be called when * the property changes. The arguments of the method are * `(value, previousValue)`. * - `computed` (string): String describing method and dependent properties * for computing the value of this property (e.g. `'computeFoo(bar, zot)'`). * Computed properties are read-only by default and can only be changed * via the return value of the computing method. * * - `static get observers()`: Array of strings describing multi-property * observer methods and their dependent properties (e.g. * `'observeABC(a, b, c)'`). * * The base class provides default implementations for the following standard * custom element lifecycle callbacks; users may override these, but should * call the super method to ensure * - `constructor`: Run when the element is created or upgraded * - `connectedCallback`: Run each time the element is connected to the * document * - `disconnectedCallback`: Run each time the element is disconnected from * the document * - `attributeChangedCallback`: Run each time an attribute in * `observedAttributes` is set or removed (note: this element's default * `observedAttributes` implementation will automatically return an array * of dash-cased attributes based on `properties`) * * @mixinFunction * @polymer * @appliesMixin Polymer.PropertyEffects * @appliesMixin Polymer.PropertiesMixin * @memberof Polymer * @property rootPath {string} Set to the value of `Polymer.rootPath`, * which defaults to the main document path * @property importPath {string} Set to the value of the class's static * `importPath` property, which defaults to the path of this element's * `dom-module` (when `is` is used), but can be overridden for other * import strategies. * @summary Element class mixin that provides the core API for Polymer's * meta-programming features. */ Polymer.ElementMixin = Polymer.dedupingMixin(base => { /** * @constructor * @extends {base} * @implements {Polymer_PropertyEffects} * @implements {Polymer_PropertiesMixin} * @private */ const polymerElementBase = Polymer.PropertiesMixin(Polymer.PropertyEffects(base)); /** * Returns a list of properties with default values. * This list is created as an optimization since it is a subset of * the list returned from `_properties`. * This list is used in `_initializeProperties` to set property defaults. * * @param {PolymerElementConstructor} constructor Element class * @return {PolymerElementProperties} Flattened properties for this class * that have default values * @private */ function propertyDefaults(constructor) { if (!constructor.hasOwnProperty( JSCompiler_renameProperty('__propertyDefaults', constructor))) { constructor.__propertyDefaults = null; let props = constructor._properties; for (let p in props) { let info = props[p]; if ('value' in info) { constructor.__propertyDefaults = constructor.__propertyDefaults || {}; constructor.__propertyDefaults[p] = info; } } } return constructor.__propertyDefaults; } /** * Returns a memoized version of the `observers` array. * @param {PolymerElementConstructor} constructor Element class * @return {Array} Array containing own observers for the given class * @protected */ function ownObservers(constructor) { if (!constructor.hasOwnProperty( JSCompiler_renameProperty('__ownObservers', constructor))) { constructor.__ownObservers = constructor.hasOwnProperty(JSCompiler_renameProperty('observers', constructor)) ? /** @type {PolymerElementConstructor} */ (constructor).observers : null; } return constructor.__ownObservers; } /** * Creates effects for a property. * * Note, once a property has been set to * `readOnly`, `computed`, `reflectToAttribute`, or `notify` * these values may not be changed. For example, a subclass cannot * alter these settings. However, additional `observers` may be added * by subclasses. * * The info object should contain property metadata as follows: * * * `type`: {function} type to which an attribute matching the property * is deserialized. Note the property is camel-cased from a dash-cased * attribute. For example, 'foo-bar' attribute is deserialized to a * property named 'fooBar'. * * * `readOnly`: {boolean} creates a readOnly property and * makes a private setter for the private of the form '_setFoo' for a * property 'foo', * * * `computed`: {string} creates a computed property. A computed property * is also automatically set to `readOnly: true`. The value is calculated * by running a method and arguments parsed from the given string. For * example 'compute(foo)' will compute a given property when the * 'foo' property changes by executing the 'compute' method. This method * must return the computed value. * * * `reflectToAttribute`: {boolean} If true, the property value is reflected * to an attribute of the same name. Note, the attribute is dash-cased * so a property named 'fooBar' is reflected as 'foo-bar'. * * * `notify`: {boolean} sends a non-bubbling notification event when * the property changes. For example, a property named 'foo' sends an * event named 'foo-changed' with `event.detail` set to the value of * the property. * * * observer: {string} name of a method that runs when the property * changes. The arguments of the method are (value, previousValue). * * Note: Users may want control over modifying property * effects via subclassing. For example, a user might want to make a * reflectToAttribute property not do so in a subclass. We've chosen to * disable this because it leads to additional complication. * For example, a readOnly effect generates a special setter. If a subclass * disables the effect, the setter would fail unexpectedly. * Based on feedback, we may want to try to make effects more malleable * and/or provide an advanced api for manipulating them. * Also consider adding warnings when an effect cannot be changed. * * @param {!PolymerElement} proto Element class prototype to add accessors * and effects to * @param {string} name Name of the property. * @param {Object} info Info object from which to create property effects. * Supported keys: * @param {Object} allProps Flattened map of all properties defined in this * element (including inherited properties) * @return {void} * @private */ function createPropertyFromConfig(proto, name, info, allProps) { // computed forces readOnly... if (info.computed) { info.readOnly = true; } // Note, since all computed properties are readOnly, this prevents // adding additional computed property effects (which leads to a confusing // setup where multiple triggers for setting a property) // While we do have `hasComputedEffect` this is set on the property's // dependencies rather than itself. if (info.computed && !proto._hasReadOnlyEffect(name)) { proto._createComputedProperty(name, info.computed, allProps); } if (info.readOnly && !proto._hasReadOnlyEffect(name)) { proto._createReadOnlyProperty(name, !info.computed); } if (info.reflectToAttribute && !proto._hasReflectEffect(name)) { proto._createReflectedProperty(name); } if (info.notify && !proto._hasNotifyEffect(name)) { proto._createNotifyingProperty(name); } // always add observer if (info.observer) { proto._createPropertyObserver(name, info.observer, allProps[info.observer]); } // always create the mapping from attribute back to property for deserialization. proto._addPropertyToAttributeMap(name); } /** * Process all style elements in the element template. Styles with the * `include` attribute are processed such that any styles in * the associated "style modules" are included in the element template. * @param {PolymerElementConstructor} klass Element class * @param {!HTMLTemplateElement} template Template to process * @param {string} is Name of element * @param {string} baseURI Base URI for element * @private */ function processElementStyles(klass, template, is, baseURI) { if (!builtCSS) { const templateStyles = template.content.querySelectorAll('style'); const stylesWithImports = Polymer.StyleGather.stylesFromTemplate(template); // insert styles from <link rel="import" type="css"> at the top of the template const linkedStyles = Polymer.StyleGather.stylesFromModuleImports(is); const firstTemplateChild = template.content.firstElementChild; for (let idx = 0; idx < linkedStyles.length; idx++) { let s = linkedStyles[idx]; s.textContent = klass._processStyleText(s.textContent, baseURI); template.content.insertBefore(s, firstTemplateChild); } // keep track of the last "concrete" style in the template we have encountered let templateStyleIndex = 0; // ensure all gathered styles are actually in this template. for (let i = 0; i < stylesWithImports.length; i++) { let s = stylesWithImports[i]; let templateStyle = templateStyles[templateStyleIndex]; // if the style is not in this template, it's been "included" and // we put a clone of it in the template before the style that included it if (templateStyle !== s) { s = s.cloneNode(true); templateStyle.parentNode.insertBefore(s, templateStyle); } else { templateStyleIndex++; } s.textContent = klass._processStyleText(s.textContent, baseURI); } } if (window.ShadyCSS) { window.ShadyCSS.prepareTemplate(template, is); } } /** * Look up template from dom-module for element * * @param {!string} is Element name to look up * @return {!HTMLTemplateElement} Template found in dom module, or * undefined if not found * @protected */ function getTemplateFromDomModule(is) { let template = null; if (is && Polymer.DomModule) { template = Polymer.DomModule.import(is, 'template'); // Under strictTemplatePolicy, require any element with an `is` // specified to have a dom-module if (Polymer.strictTemplatePolicy && !template) { throw new Error(`strictTemplatePolicy: expecting dom-module or null template for ${is}`); } } return template; } /** * @polymer * @mixinClass * @unrestricted * @implements {Polymer_ElementMixin} */ class PolymerElement extends polymerElementBase { /** * Override of PropertiesMixin _finalizeClass to create observers and * find the template. * @return {void} * @protected * @override * @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do */ static _finalizeClass() { super._finalizeClass(); const observers = ownObservers(this); if (observers) { this.createObservers(observers, this._properties); } this._prepareTemplate(); } static _prepareTemplate() { // note: create "working" template that is finalized at instance time let template = /** @type {PolymerElementConstructor} */ (this).template; if (template) { if (typeof template === 'string') { let t = document.createElement('template'); t.innerHTML = template; template = t; } else if (!Polymer.legacyOptimizations) { template = template.cloneNode(true); } } this.prototype._template = template; } /** * Override of PropertiesChanged createProperties to create accessors * and property effects for all of the properties. * @return {void} * @protected * @override */ static createProperties(props) { for (let p in props) { createPropertyFromConfig(this.prototype, p, props[p], props); } } /** * Creates observers for the given `observers` array. * Leverages `PropertyEffects` to create observers. * @param {Object} observers Array of observer descriptors for * this class * @param {Object} dynamicFns Object containing keys for any properties * that are functions and should trigger the effect when the function * reference is changed * @return {void} * @protected */ static createObservers(observers, dynamicFns) { const proto = this.prototype; for (let i=0; i < observers.length; i++) { proto._createMethodObserver(observers[i], dynamicFns); } } /** * Returns the template that will be stamped into this element's shadow root. * * If a `static get is()` getter is defined, the default implementation * will return the first `<template>` in a `dom-module` whose `id` * matches this element's `is`. * * Users may override this getter to return an arbitrary template * (in which case the `is` getter is unnecessary). The template returned * may be either an `HTMLTemplateElement` or a string that will be * automatically parsed into a template. * * Note that when subclassing, if the super class overrode the default * implementation and the subclass would like to provide an alternate * template via a `dom-module`, it should override this getter and * return `Polymer.DomModule.import(this.is, 'template')`. * * If a subclass would like to modify the super class template, it should * clone it rather than modify it in place. If the getter does expensive * work such as cloning/modifying a template, it should memoize the * template for maximum performance: * * let memoizedTemplate; * class MySubClass extends MySuperClass { * static get template() { * if (!memoizedTemplate) { * memoizedTemplate = MySuperClass.template.cloneNode(true); * let subContent = document.createElement('div'); * subContent.textContent = 'This came from MySubClass'; * memoizedTemplate.content.appendChild(subContent); * } * return memoizedTemplate; * } * } * * @return {HTMLTemplateElement|string} Template to be stamped */ static get template() { // Explanation of template-related properties: // - constructor.template (this getter): the template for the class. // This can come from the prototype (for legacy elements), from a // dom-module, or from the super class's template (or can be overridden // altogether by the user) // - constructor._template: memoized version of constructor.template // - prototype._template: working template for the element, which will be // parsed and modified in place. It is a cloned version of // constructor.template, saved in _finalizeClass(). Note that before // this getter is called, for legacy elements this could be from a // _template field on the info object passed to Polymer(), a behavior, // or set in registered(); once the static getter runs, a clone of it // will overwrite it on the prototype as the working template. if (!this.hasOwnProperty(JSCompiler_renameProperty('_template', this))) { this._template = // If user has put template on prototype (e.g. in legacy via registered // callback or info object), prefer that first this.prototype.hasOwnProperty(JSCompiler_renameProperty('_template', this.prototype)) ? this.prototype._template : // Look in dom-module associated with this element's is (getTemplateFromDomModule(/** @type {PolymerElementConstructor}*/ (this).is) || // Next look for superclass template (call the super impl this // way so that `this` points to the superclass) Object.getPrototypeOf(/** @type {PolymerElementConstructor}*/ (this).prototype).constructor.template); } return this._template; } /** * Set the template. * * @param {!HTMLTemplateElement|string} value Template to set. */ static set template(value) { this._template = value; } /** * Path matching the url from which the element was imported. * * This path is used to resolve url's in template style cssText. * The `importPath` property is also set on element instances and can be * used to create bindings relative to the import path. * * For elements defined in ES modules, users should implement * `static get importMeta() { return import.meta; }`, and the default * implementation of `importPath` will return `import.meta.url`'s path. * For elements defined in HTML imports, this getter will return the path * to the document containing a `dom-module` element matching this * element's static `is` property. * * Note, this path should contain a trailing `/`. * * @return {string} The import path for this element class * @suppress {missingProperties} */ static get importPath() { if (!this.hasOwnProperty(JSCompiler_renameProperty('_importPath', this))) { const meta = this.importMeta; if (meta) { this._importPath = Polymer.ResolveUrl.pathFromUrl(meta.url); } else { const module = Polymer.DomModule && Polymer.DomModule.import(/** @type {PolymerElementConstructor} */ (this).is); this._importPath = (module && module.assetpath) || Object.getPrototypeOf(/** @type {PolymerElementConstructor}*/ (this).prototype).constructor.importPath; } } return this._importPath; } constructor() { super(); /** @type {HTMLTemplateElement} */ this._template; /** @type {string} */ this._importPath; /** @type {string} */ this.rootPath; /** @type {string} */ this.importPath; /** @type {StampedTemplate | HTMLElement | ShadowRoot} */ this.root; /** @type {!Object<string, !Element>} */ this.$; } /** * Overrides the default `Polymer.PropertyAccessors` to ensure class * metaprogramming related to property accessors and effects has * completed (calls `finalize`). * * It also initializes any property defaults provided via `value` in * `properties` metadata. * * @return {void} * @override * @suppress {invalidCasts} */ _initializeProperties() { this.constructor.finalize(); // note: finalize template when we have access to `localName` to // avoid dependence on `is` for polyfilling styling. this.constructor._finalizeTemplate(/** @type {!HTMLElement} */(this).localName); super._initializeProperties(); // set path defaults this.rootPath = Polymer.rootPath; this.importPath = this.constructor.importPath; // apply property defaults... let p$ = propertyDefaults(this.constructor); if (!p$) { return; } for (let p in p$) { let info = p$[p]; // Don't set default value if there is already an own property, which // happens when a `properties` property with default but no effects had // a property set (e.g. bound) by its host before upgrade if (!this.hasOwnProperty(p)) { let value = typeof info.value == 'function' ? info.value.call(this) : info.value; // Set via `_setProperty` if there is an accessor, to enable // initializing readOnly property defaults if (this._hasAccessor(p)) { this._setPendingProperty(p, value, true); } else { this[p] = value; } } } } /** * Gather style text for a style element in the template. * * @param {string} cssText Text containing styling to process * @param {string} baseURI Base URI to rebase CSS paths against * @return {string} The processed CSS text * @protected */ static _processStyleText(cssText, baseURI) { return Polymer.ResolveUrl.resolveCss(cssText, baseURI); } /** * Configures an element `proto` to function with a given `template`. * The element name `is` and extends `ext` must be specified for ShadyCSS * style scoping. * * @param {string} is Tag name (or type extension name) for this element * @return {void} * @protected */ static _finalizeTemplate(is) { /** @const {HTMLTemplateElement} */ const template = this.prototype._template; if (template && !template.__polymerFinalized) { template.__polymerFinalized = true; const importPath = this.importPath; const baseURI = importPath ? Polymer.ResolveUrl.resolveUrl(importPath) : ''; // e.g. support `include="module-name"`, and ShadyCSS processElementStyles(this, template, is, baseURI); this.prototype._bindTemplate(template); } } /** * Provides a default implementation of the standard Custom Elements * `connectedCallback`. * * The default implementation enables the property effects system and * flushes any pending properties, and updates shimmed CSS properties * when using the ShadyCSS scoping/custom properties polyfill. * * @suppress {missingProperties, invalidCasts} Super may or may not implement the callback * @return {void} */ connectedCallback() { if (window.ShadyCSS && this._template) { window.ShadyCSS.styleElement(/** @type {!HTMLElement} */(this)); } super.connectedCallback(); } /** * Stamps the element template. * * @return {void} * @override */ ready() { if (this._template) { this.root = this._stampTemplate(this._template); this.$ = this.root.$; } super.ready(); } /** * Implements `PropertyEffects`'s `_readyClients` call. Attaches * element dom by calling `_attachDom` with the dom stamped from the * element's template via `_stampTemplate`. Note that this allows * client dom to be attached to the element prior to any observers * running. * * @return {void} * @override */ _readyClients() { if (this._template) { this.root = this._attachDom(/** @type {StampedTemplate} */(this.root)); } // The super._readyClients here sets the clients initialized flag. // We must wait to do this until after client dom is created/attached // so that this flag can be checked to prevent notifications fired // during this process from being handled before clients are ready. super._readyClients(); } /** * Attaches an element's stamped dom to itself. By default, * this method creates a `shadowRoot` and adds the dom to it. * However, this method may be overridden to allow an element * to put its dom in another location. * * @throws {Error} * @suppress {missingReturn} * @param {StampedTemplate} dom to attach to the element. * @return {ShadowRoot} node to which the dom has been attached. */ _attachDom(dom) { if (this.attachShadow) { if (dom) { if (!this.shadowRoot) { this.attachShadow({mode: 'open'}); } this.shadowRoot.appendChild(dom); return this.shadowRoot; } return null; } else { throw new Error('ShadowDOM not available. ' + // TODO(sorvell): move to compile-time conditional when supported 'Polymer.Element can create dom as children instead of in ' + 'ShadowDOM by setting `this.root = this;\` before \`ready\`.'); } } /** * When using the ShadyCSS scoping and custom property shim, causes all * shimmed styles in this element (and its subtree) to be updated * based on current custom property values. * * The optional parameter overrides inline custom property styles with an * object of properties where the keys are CSS properties, and the values * are strings. * * Example: `this.updateStyles({'--color': 'blue'})` * * These properties are retained unless a value of `null` is set. * * Note: This function does not support updating CSS mixins. * You can not dynamically change the value of an `@apply`. * * @param {Object=} properties Bag of custom property key/values to * apply to this element. * @return {void} * @suppress {invalidCasts} */ updateStyles(properties) { if (window.ShadyCSS) { window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */(this), properties); } } /** * Rewrites a given URL relative to a base URL. The base URL defaults to * the original location of the document containing the `dom-module` for * this element. This method will return the same URL before and after * bundling. * * Note that this function performs no resolution for URLs that start * with `/` (absolute URLs) or `#` (hash identifiers). For general purpose * URL resolution, use `window.URL`. * * @param {string} url URL to resolve. * @param {string=} base Optional base URL to resolve against, defaults * to the element's `importPath` * @return {string} Rewritten URL relative to base */ resolveUrl(url, base) { if (!base && this.importPath) { base = Polymer.ResolveUrl.resolveUrl(this.importPath); } return Polymer.ResolveUrl.resolveUrl(url, base); } /** * Overrides `PropertyAccessors` to add map of dynamic functions on * template info, for consumption by `PropertyEffects` template binding * code. This map determines which method templates should have accessors * created for them. * * @override * @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do */ static _parseTemplateContent(template, templateInfo, nodeInfo) { templateInfo.dynamicFns = templateInfo.dynamicFns || this._properties; return super._parseTemplateContent(template, templateInfo, nodeInfo); } } return PolymerElement; }); /** * When using the ShadyCSS scoping and custom property shim, causes all * shimmed `styles` (via `custom-style`) in the document (and its subtree) * to be updated based on current custom property values. * * The optional parameter overrides inline custom property styles with an * object of properties where the keys are CSS properties, and the values * are strings. * * Example: `Polymer.updateStyles({'--color': 'blue'})` * * These properties are retained unless a value of `null` is set. * * @param {Object=} props Bag of custom property key/values to * apply to the document. * @return {void} */ Polymer.updateStyles = function(props) { if (window.ShadyCSS) { window.ShadyCSS.styleDocument(props); } }; })(); </script>