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

702 lines (670 loc) 27.1 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 */ /** * Module for preparing and stamping instances of templates that utilize * Polymer's data-binding and declarative event listener features. * * Example: * * // Get a template from somewhere, e.g. light DOM * let template = this.querySelector('template'); * // Prepare the template * let TemplateClass = Templatize.templatize(template); * // Instance the template with an initial data model * let instance = new TemplateClass({myProp: 'initial'}); * // Insert the instance's DOM somewhere, e.g. element's shadow DOM * this.shadowRoot.appendChild(instance.root); * // Changing a property on the instance will propagate to bindings * // in the template * instance.myProp = 'new value'; * * The `options` dictionary passed to `templatize` allows for customizing * features of the generated template class, including how outer-scope host * properties should be forwarded into template instances, how any instance * properties added into the template's scope should be notified out to * the host, and whether the instance should be decorated as a "parent model" * of any event handlers. * * // Customize property forwarding and event model decoration * let TemplateClass = Templatize.templatize(template, this, { * parentModel: true, * forwardHostProp(property, value) {...}, * instanceProps: {...}, * notifyInstanceProp(instance, property, value) {...}, * }); * * @summary Module for preparing and stamping instances of templates * utilizing Polymer templating features. */ import './boot.js'; import { PropertyEffects } from '../mixins/property-effects.js'; import { MutableData } from '../mixins/mutable-data.js'; import { strictTemplatePolicy, legacyWarnings } from './settings.js'; import { wrap } from './wrap.js'; // Base class for HTMLTemplateElement extension that has property effects // machinery for propagating host properties to children. This is an ES5 // class only because Babel (incorrectly) requires super() in the class // constructor even though no `this` is used and it returns an instance. let newInstance = null; /** * @constructor * @extends {HTMLTemplateElement} * @private */ function HTMLTemplateElementExtension() { return newInstance; } HTMLTemplateElementExtension.prototype = Object.create(HTMLTemplateElement.prototype, { constructor: { value: HTMLTemplateElementExtension, writable: true } }); /** * @constructor * @implements {Polymer_PropertyEffects} * @extends {HTMLTemplateElementExtension} * @private */ const DataTemplate = PropertyEffects(HTMLTemplateElementExtension); /** * @constructor * @implements {Polymer_MutableData} * @extends {DataTemplate} * @private */ const MutableDataTemplate = MutableData(DataTemplate); // Applies a DataTemplate subclass to a <template> instance function upgradeTemplate(template, constructor) { newInstance = template; Object.setPrototypeOf(template, constructor.prototype); new constructor(); newInstance = null; } /** * Base class for TemplateInstance. * @constructor * @extends {HTMLElement} * @implements {Polymer_PropertyEffects} * @private */ const templateInstanceBase = PropertyEffects(class {}); export function showHideChildren(hide, children) { for (let i=0; i<children.length; i++) { let n = children[i]; // Ignore non-changes if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) { // clear and restore text if (n.nodeType === Node.TEXT_NODE) { if (hide) { n.__polymerTextContent__ = n.textContent; n.textContent = ''; } else { n.textContent = n.__polymerTextContent__; } // remove and replace slot } else if (n.localName === 'slot') { if (hide) { n.__polymerReplaced__ = document.createComment('hidden-slot'); wrap(wrap(n).parentNode).replaceChild(n.__polymerReplaced__, n); } else { const replace = n.__polymerReplaced__; if (replace) { wrap(wrap(replace).parentNode).replaceChild(n, replace); } } } // hide and show nodes else if (n.style) { if (hide) { n.__polymerDisplay__ = n.style.display; n.style.display = 'none'; } else { n.style.display = n.__polymerDisplay__; } } } n.__hideTemplateChildren__ = hide; if (n._showHideChildren) { n._showHideChildren(hide); } } } /** * @polymer * @customElement * @appliesMixin PropertyEffects * @unrestricted */ class TemplateInstanceBase extends templateInstanceBase { constructor(props) { super(); this._configureProperties(props); /** @type {!StampedTemplate} */ this.root = this._stampTemplate(this.__dataHost); // Save list of stamped children let children = []; /** @suppress {invalidCasts} */ this.children = /** @type {!NodeList} */ (children); // Polymer 1.x did not use `Polymer.dom` here so not bothering. for (let n = this.root.firstChild; n; n=n.nextSibling) { children.push(n); n.__templatizeInstance = this; } if (this.__templatizeOwner && this.__templatizeOwner.__hideTemplateChildren__) { this._showHideChildren(true); } // Flush props only when props are passed if instance props exist // or when there isn't instance props. let options = this.__templatizeOptions; if ((props && options.instanceProps) || !options.instanceProps) { this._enableProperties(); } } /** * Configure the given `props` by calling `_setPendingProperty`. Also * sets any properties stored in `__hostProps`. * @private * @param {Object} props Object of property name-value pairs to set. * @return {void} */ _configureProperties(props) { let options = this.__templatizeOptions; if (options.forwardHostProp) { for (let hprop in this.__hostProps) { this._setPendingProperty(hprop, this.__dataHost['_host_' + hprop]); } } // Any instance props passed in the constructor will overwrite host props; // normally this would be a user error but we don't specifically filter them for (let iprop in props) { this._setPendingProperty(iprop, props[iprop]); } } /** * Forwards a host property to this instance. This method should be * called on instances from the `options.forwardHostProp` callback * to propagate changes of host properties to each instance. * * Note this method enqueues the change, which are flushed as a batch. * * @param {string} prop Property or path name * @param {*} value Value of the property to forward * @return {void} */ forwardHostProp(prop, value) { if (this._setPendingPropertyOrPath(prop, value, false, true)) { this.__dataHost._enqueueClient(this); } } /** * Override point for adding custom or simulated event handling. * * @override * @param {!Node} node Node to add event listener to * @param {string} eventName Name of event * @param {function(!Event):void} handler Listener function to add * @return {void} */ _addEventListenerToNode(node, eventName, handler) { if (this._methodHost && this.__templatizeOptions.parentModel) { // If this instance should be considered a parent model, decorate // events this template instance as `model` this._methodHost._addEventListenerToNode(node, eventName, (e) => { e.model = this; handler(e); }); } else { // Otherwise delegate to the template's host (which could be) // another template instance let templateHost = this.__dataHost.__dataHost; if (templateHost) { templateHost._addEventListenerToNode(node, eventName, handler); } } } /** * Shows or hides the template instance top level child elements. For * text nodes, `textContent` is removed while "hidden" and replaced when * "shown." * @param {boolean} hide Set to true to hide the children; * set to false to show them. * @return {void} * @protected */ _showHideChildren(hide) { showHideChildren(hide, this.children); } /** * Overrides default property-effects implementation to intercept * textContent bindings while children are "hidden" and cache in * private storage for later retrieval. * * @override * @param {!Node} node The node to set a property on * @param {string} prop The property to set * @param {*} value The value to set * @return {void} * @protected */ _setUnmanagedPropertyToNode(node, prop, value) { if (node.__hideTemplateChildren__ && node.nodeType == Node.TEXT_NODE && prop == 'textContent') { node.__polymerTextContent__ = value; } else { super._setUnmanagedPropertyToNode(node, prop, value); } } /** * Find the parent model of this template instance. The parent model * is either another templatize instance that had option `parentModel: true`, * or else the host element. * * @return {!Polymer_PropertyEffects} The parent model of this instance */ get parentModel() { let model = this.__parentModel; if (!model) { let options; model = this; do { // A template instance's `__dataHost` is a <template> // `model.__dataHost.__dataHost` is the template's host model = model.__dataHost.__dataHost; } while ((options = model.__templatizeOptions) && !options.parentModel); this.__parentModel = model; } return model; } /** * Stub of HTMLElement's `dispatchEvent`, so that effects that may * dispatch events safely no-op. * * @param {Event} event Event to dispatch * @return {boolean} Always true. * @override */ dispatchEvent(event) { // eslint-disable-line no-unused-vars return true; } } /** @type {!DataTemplate} */ TemplateInstanceBase.prototype.__dataHost; /** @type {!TemplatizeOptions} */ TemplateInstanceBase.prototype.__templatizeOptions; /** @type {!Polymer_PropertyEffects} */ TemplateInstanceBase.prototype._methodHost; /** @type {!Object} */ TemplateInstanceBase.prototype.__templatizeOwner; /** @type {!Object} */ TemplateInstanceBase.prototype.__hostProps; /** * @constructor * @extends {TemplateInstanceBase} * @implements {Polymer_MutableData} * @private */ const MutableTemplateInstanceBase = MutableData( // This cast shouldn't be neccessary, but Closure doesn't understand that // TemplateInstanceBase is a constructor function. /** @type {function(new:TemplateInstanceBase)} */ (TemplateInstanceBase)); function findMethodHost(template) { // Technically this should be the owner of the outermost template. // In shadow dom, this is always getRootNode().host, but we can // approximate this via cooperation with our dataHost always setting // `_methodHost` as long as there were bindings (or id's) on this // instance causing it to get a dataHost. let templateHost = template.__dataHost; return templateHost && templateHost._methodHost || templateHost; } /* eslint-disable valid-jsdoc */ /** * @suppress {missingProperties} class.prototype is not defined for some reason */ function createTemplatizerClass(template, templateInfo, options) { /** * @constructor * @extends {TemplateInstanceBase} */ let templatizerBase = options.mutableData ? MutableTemplateInstanceBase : TemplateInstanceBase; // Affordance for global mixins onto TemplatizeInstance if (templatize.mixin) { templatizerBase = templatize.mixin(templatizerBase); } /** * Anonymous class created by the templatize * @constructor * @private */ let klass = class extends templatizerBase { }; /** @override */ klass.prototype.__templatizeOptions = options; klass.prototype._bindTemplate(template); addNotifyEffects(klass, template, templateInfo, options); return klass; } /** * Adds propagate effects from the template to the template instance for * properties that the host binds to the template using the `_host_` prefix. * * @suppress {missingProperties} class.prototype is not defined for some reason */ function addPropagateEffects(target, templateInfo, options, methodHost) { let userForwardHostProp = options.forwardHostProp; if (userForwardHostProp && templateInfo.hasHostProps) { // Under the `removeNestedTemplates` optimization, a custom element like // `dom-if` or `dom-repeat` can itself be treated as the "template"; this // flag is used to switch between upgrading a `<template>` to be a property // effects client vs. adding the effects directly to the custom element const isTemplate = target.localName == 'template'; // Provide data API and property effects on memoized template class let klass = templateInfo.templatizeTemplateClass; if (!klass) { if (isTemplate) { /** * @constructor * @extends {DataTemplate} */ let templatizedBase = options.mutableData ? MutableDataTemplate : DataTemplate; // NOTE: due to https://github.com/google/closure-compiler/issues/2928, // combining the next two lines into one assignment causes a spurious // type error. /** @private */ class TemplatizedTemplate extends templatizedBase {} klass = templateInfo.templatizeTemplateClass = TemplatizedTemplate; } else { /** * @constructor * @extends {PolymerElement} */ const templatizedBase = target.constructor; // Create a cached subclass of the base custom element class onto which // to put the template-specific propagate effects // NOTE: due to https://github.com/google/closure-compiler/issues/2928, // combining the next two lines into one assignment causes a spurious // type error. /** @private */ class TemplatizedTemplateExtension extends templatizedBase {} klass = templateInfo.templatizeTemplateClass = TemplatizedTemplateExtension; } // Add template - >instances effects // and host <- template effects let hostProps = templateInfo.hostProps; for (let prop in hostProps) { klass.prototype._addPropertyEffect('_host_' + prop, klass.prototype.PROPERTY_EFFECT_TYPES.PROPAGATE, {fn: createForwardHostPropEffect(prop, userForwardHostProp)}); klass.prototype._createNotifyingProperty('_host_' + prop); } if (legacyWarnings && methodHost) { warnOnUndeclaredProperties(templateInfo, options, methodHost); } } // Mix any pre-bound data into __data; no need to flush this to // instances since they pull from the template at instance-time if (target.__dataProto) { // Note, generally `__dataProto` could be chained, but it's guaranteed // to not be since this is a vanilla template we just added effects to Object.assign(target.__data, target.__dataProto); } if (isTemplate) { upgradeTemplate(target, klass); // Clear any pending data for performance target.__dataTemp = {}; target.__dataPending = null; target.__dataOld = null; target._enableProperties(); } else { // Swizzle the cached subclass prototype onto the custom element Object.setPrototypeOf(target, klass.prototype); // Check for any pre-bound instance host properties, and do the // instance property delete/assign dance for those (directly into data; // not need to go through accessor since they are pulled at instance time) const hostProps = templateInfo.hostProps; for (let prop in hostProps) { prop = '_host_' + prop; if (prop in target) { const val = target[prop]; delete target[prop]; target.__data[prop] = val; } } } } } /* eslint-enable valid-jsdoc */ function createForwardHostPropEffect(hostProp, userForwardHostProp) { return function forwardHostProp(template, prop, props) { userForwardHostProp.call(template.__templatizeOwner, prop.substring('_host_'.length), props[prop]); }; } function addNotifyEffects(klass, template, templateInfo, options) { let hostProps = templateInfo.hostProps || {}; for (let iprop in options.instanceProps) { delete hostProps[iprop]; let userNotifyInstanceProp = options.notifyInstanceProp; if (userNotifyInstanceProp) { klass.prototype._addPropertyEffect(iprop, klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY, {fn: createNotifyInstancePropEffect(iprop, userNotifyInstanceProp)}); } } if (options.forwardHostProp && template.__dataHost) { for (let hprop in hostProps) { // As we're iterating hostProps in this function, note whether // there were any, for an optimization in addPropagateEffects if (!templateInfo.hasHostProps) { templateInfo.hasHostProps = true; } klass.prototype._addPropertyEffect(hprop, klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY, {fn: createNotifyHostPropEffect()}); } } } function createNotifyInstancePropEffect(instProp, userNotifyInstanceProp) { return function notifyInstanceProp(inst, prop, props) { userNotifyInstanceProp.call(inst.__templatizeOwner, inst, prop, props[prop]); }; } function createNotifyHostPropEffect() { return function notifyHostProp(inst, prop, props) { inst.__dataHost._setPendingPropertyOrPath('_host_' + prop, props[prop], true, true); }; } /** * Returns an anonymous `PropertyEffects` class bound to the * `<template>` provided. Instancing the class will result in the * template being stamped into a document fragment stored as the instance's * `root` property, after which it can be appended to the DOM. * * Templates may utilize all Polymer data-binding features as well as * declarative event listeners. Event listeners and inline computing * functions in the template will be called on the host of the template. * * The constructor returned takes a single argument dictionary of initial * property values to propagate into template bindings. Additionally * host properties can be forwarded in, and instance properties can be * notified out by providing optional callbacks in the `options` dictionary. * * Valid configuration in `options` are as follows: * * - `forwardHostProp(property, value)`: Called when a property referenced * in the template changed on the template's host. As this library does * not retain references to templates instanced by the user, it is the * templatize owner's responsibility to forward host property changes into * user-stamped instances. The `instance.forwardHostProp(property, value)` * method on the generated class should be called to forward host * properties into the template to prevent unnecessary property-changed * notifications. Any properties referenced in the template that are not * defined in `instanceProps` will be notified up to the template's host * automatically. * - `instanceProps`: Dictionary of property names that will be added * to the instance by the templatize owner. These properties shadow any * host properties, and changes within the template to these properties * will result in `notifyInstanceProp` being called. * - `mutableData`: When `true`, the generated class will skip strict * dirty-checking for objects and arrays (always consider them to be * "dirty"). * - `notifyInstanceProp(instance, property, value)`: Called when * an instance property changes. Users may choose to call `notifyPath` * on e.g. the owner to notify the change. * - `parentModel`: When `true`, events handled by declarative event listeners * (`on-event="handler"`) will be decorated with a `model` property pointing * to the template instance that stamped it. It will also be returned * from `instance.parentModel` in cases where template instance nesting * causes an inner model to shadow an outer model. * * All callbacks are called bound to the `owner`. Any context * needed for the callbacks (such as references to `instances` stamped) * should be stored on the `owner` such that they can be retrieved via * `this`. * * When `options.forwardHostProp` is declared as an option, any properties * referenced in the template will be automatically forwarded from the host of * the `<template>` to instances, with the exception of any properties listed in * the `options.instanceProps` object. `instanceProps` are assumed to be * managed by the owner of the instances, either passed into the constructor * or set after the fact. Note, any properties passed into the constructor will * always be set to the instance (regardless of whether they would normally * be forwarded from the host). * * Note that `templatize()` can be run only once for a given `<template>`. * Further calls will result in an error. Also, there is a special * behavior if the template was duplicated through a mechanism such as * `<dom-repeat>` or `<test-fixture>`. In this case, all calls to * `templatize()` return the same class for all duplicates of a template. * The class returned from `templatize()` is generated only once using * the `options` from the first call. This means that any `options` * provided to subsequent calls will be ignored. Therefore, it is very * important not to close over any variables inside the callbacks. Also, * arrow functions must be avoided because they bind the outer `this`. * Inside the callbacks, any contextual information can be accessed * through `this`, which points to the `owner`. * * @param {!HTMLTemplateElement} template Template to templatize * @param {Polymer_PropertyEffects=} owner Owner of the template instances; * any optional callbacks will be bound to this owner. * @param {Object=} options Options dictionary (see summary for details) * @return {function(new:TemplateInstanceBase, Object=)} Generated class bound * to the template provided * @suppress {invalidCasts} */ export function templatize(template, owner, options) { // Under strictTemplatePolicy, the templatized element must be owned // by a (trusted) Polymer element, indicated by existence of _methodHost; // e.g. for dom-if & dom-repeat in main document, _methodHost is null if (strictTemplatePolicy && !findMethodHost(template)) { throw new Error('strictTemplatePolicy: template owner not trusted'); } options = /** @type {!TemplatizeOptions} */(options || {}); if (template.__templatizeOwner) { throw new Error('A <template> can only be templatized once'); } template.__templatizeOwner = owner; const ctor = owner ? owner.constructor : TemplateInstanceBase; let templateInfo = ctor._parseTemplate(template); // Get memoized base class for the prototypical template, which // includes property effects for binding template & forwarding /** * @constructor * @extends {TemplateInstanceBase} */ let baseClass = templateInfo.templatizeInstanceClass; if (!baseClass) { baseClass = createTemplatizerClass(template, templateInfo, options); templateInfo.templatizeInstanceClass = baseClass; } const methodHost = findMethodHost(template); // Host property forwarding must be installed onto template instance addPropagateEffects(template, templateInfo, options, methodHost); // Subclass base class and add reference for this specific template /** @private */ let klass = class TemplateInstance extends baseClass {}; /** @override */ klass.prototype._methodHost = methodHost; /** @override */ klass.prototype.__dataHost = /** @type {!DataTemplate} */ (template); /** @override */ klass.prototype.__templatizeOwner = /** @type {!Object} */ (owner); /** @override */ klass.prototype.__hostProps = templateInfo.hostProps; klass = /** @type {function(new:TemplateInstanceBase)} */(klass); //eslint-disable-line no-self-assign return klass; } function warnOnUndeclaredProperties(templateInfo, options, methodHost) { const declaredProps = methodHost.constructor._properties; const {propertyEffects} = templateInfo; const {instanceProps} = options; for (let prop in propertyEffects) { // Ensure properties with template effects are declared on the outermost // host (`methodHost`), unless they are instance props or static functions if (!declaredProps[prop] && !(instanceProps && instanceProps[prop])) { const effects = propertyEffects[prop]; for (let i=0; i<effects.length; i++) { const {part} = effects[i].info; if (!(part.signature && part.signature.static)) { console.warn(`Property '${prop}' used in template but not ` + `declared in 'properties'; attribute will not be observed.`); break; } } } } } /** * Returns the template "model" associated with a given element, which * serves as the binding scope for the template instance the element is * contained in. A template model is an instance of * `TemplateInstanceBase`, and should be used to manipulate data * associated with this template instance. * * Example: * * let model = modelForElement(el); * if (model.index < 10) { * model.set('item.checked', true); * } * * @param {HTMLElement} template The model will be returned for * elements stamped from this template (accepts either an HTMLTemplateElement) * or a `<dom-if>`/`<dom-repeat>` element when using `removeNestedTemplates` * optimization. * @param {Node=} node Node for which to return a template model. * @return {TemplateInstanceBase} Template instance representing the * binding scope for the element */ export function modelForElement(template, node) { let model; while (node) { // An element with a __templatizeInstance marks the top boundary // of a scope; walk up until we find one, and then ensure that // its __dataHost matches `this`, meaning this dom-repeat stamped it if ((model = node.__dataHost ? node : node.__templatizeInstance)) { // Found an element stamped by another template; keep walking up // from its __dataHost if (model.__dataHost != template) { node = model.__dataHost; } else { return model; } } else { // Still in a template scope, keep going up until // a __templatizeInstance is found node = wrap(node).parentNode; } } return null; } export { TemplateInstanceBase };