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

668 lines (625 loc) 22.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 */ import { PolymerElement } from '../../polymer-element.js'; import { Debouncer } from '../utils/debounce.js'; import { enqueueDebouncer, flush } from '../utils/flush.js'; import { microTask } from '../utils/async.js'; import { root } from '../utils/path.js'; import { wrap } from '../utils/wrap.js'; import { hideElementsGlobally } from '../utils/hide-template-controls.js'; import { fastDomIf, strictTemplatePolicy, suppressTemplateNotifications } from '../utils/settings.js'; import { showHideChildren, templatize } from '../utils/templatize.js'; /** * @customElement * @polymer * @extends PolymerElement * @summary Base class for dom-if element; subclassed into concrete * implementation. */ class DomIfBase extends PolymerElement { // Not needed to find template; can be removed once the analyzer // can find the tag name from customElements.define call static get is() { return 'dom-if'; } static get template() { return null; } static get properties() { return { /** * Fired whenever DOM is added or removed/hidden by this template (by * default, rendering occurs lazily). To force immediate rendering, call * `render`. * * @event dom-change */ /** * A boolean indicating whether this template should stamp. */ if: { type: Boolean, observer: '__debounceRender' }, /** * When true, elements will be removed from DOM and discarded when `if` * becomes false and re-created and added back to the DOM when `if` * becomes true. By default, stamped elements will be hidden but left * in the DOM when `if` becomes false, which is generally results * in better performance. */ restamp: { type: Boolean, observer: '__debounceRender' }, /** * When the global `suppressTemplateNotifications` setting is used, setting * `notifyDomChange: true` will enable firing `dom-change` events on this * element. */ notifyDomChange: { type: Boolean } }; } constructor() { super(); this.__renderDebouncer = null; this._lastIf = false; this.__hideTemplateChildren__ = false; /** @type {!HTMLTemplateElement|undefined} */ this.__template; /** @type {!TemplateInfo|undefined} */ this._templateInfo; } __debounceRender() { // Render is async for 2 reasons: // 1. To eliminate dom creation trashing if user code thrashes `if` in the // same turn. This was more common in 1.x where a compound computed // property could result in the result changing multiple times, but is // mitigated to a large extent by batched property processing in 2.x. // 2. To avoid double object propagation when a bag including values bound // to the `if` property as well as one or more hostProps could enqueue // the <dom-if> to flush before the <template>'s host property // forwarding. In that scenario creating an instance would result in // the host props being set once, and then the enqueued changes on the // template would set properties a second time, potentially causing an // object to be set to an instance more than once. Creating the // instance async from flushing data ensures this doesn't happen. If // we wanted a sync option in the future, simply having <dom-if> flush // (or clear) its template's pending host properties before creating // the instance would also avoid the problem. this.__renderDebouncer = Debouncer.debounce( this.__renderDebouncer , microTask , () => this.__render()); enqueueDebouncer(this.__renderDebouncer); } /** * @override * @return {void} */ disconnectedCallback() { super.disconnectedCallback(); const parent = wrap(this).parentNode; if (!parent || (parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE && !wrap(parent).host)) { this.__teardownInstance(); } } /** * @override * @return {void} */ connectedCallback() { super.connectedCallback(); if (!hideElementsGlobally()) { this.style.display = 'none'; } if (this.if) { this.__debounceRender(); } } /** * Ensures a template has been assigned to `this.__template`. If it has not * yet been, it querySelectors for it in its children and if it does not yet * exist (e.g. in parser-generated case), opens a mutation observer and * waits for it to appear (returns false if it has not yet been found, * otherwise true). In the `removeNestedTemplates` case, the "template" will * be the `dom-if` element itself. * * @return {boolean} True when a template has been found, false otherwise */ __ensureTemplate() { if (!this.__template) { // When `removeNestedTemplates` is true, the "template" is the element // itself, which has been given a `_templateInfo` property const thisAsTemplate = /** @type {!HTMLTemplateElement} */ ( /** @type {!HTMLElement} */ (this)); let template = thisAsTemplate._templateInfo ? thisAsTemplate : /** @type {!HTMLTemplateElement} */ (wrap(thisAsTemplate).querySelector('template')); if (!template) { // Wait until childList changes and template should be there by then let observer = new MutationObserver(() => { if (wrap(this).querySelector('template')) { observer.disconnect(); this.__render(); } else { throw new Error('dom-if requires a <template> child'); } }); observer.observe(this, {childList: true}); return false; } this.__template = template; } return true; } /** * Ensures a an instance of the template has been created and inserted. This * method may return false if the template has not yet been found or if * there is no `parentNode` to insert the template into (in either case, * connection or the template-finding mutation observer firing will queue * another render, causing this method to be called again at a more * appropriate time). * * Subclasses should implement the following methods called here: * - `__hasInstance` * - `__createAndInsertInstance` * - `__getInstanceNodes` * * @return {boolean} True if the instance was created, false otherwise. */ __ensureInstance() { let parentNode = wrap(this).parentNode; if (!this.__hasInstance()) { // Guard against element being detached while render was queued if (!parentNode) { return false; } // Find the template (when false, there was no template yet) if (!this.__ensureTemplate()) { return false; } this.__createAndInsertInstance(parentNode); } else { // Move instance children if necessary let children = this.__getInstanceNodes(); if (children && children.length) { // Detect case where dom-if was re-attached in new position let lastChild = wrap(this).previousSibling; if (lastChild !== children[children.length-1]) { for (let i=0, n; (i<children.length) && (n=children[i]); i++) { wrap(parentNode).insertBefore(n, this); } } } } return true; } /** * Forces the element to render its content. Normally rendering is * asynchronous to a provoking change. This is done for efficiency so * that multiple changes trigger only a single render. The render method * should be called if, for example, template rendering is required to * validate application state. * * @return {void} */ render() { flush(); } /** * Performs the key rendering steps: * 1. Ensure a template instance has been stamped (when true) * 2. Remove the template instance (when false and restamp:true) * 3. Sync the hidden state of the instance nodes with the if/restamp state * 4. Fires the `dom-change` event when necessary * * @return {void} */ __render() { if (this.if) { if (!this.__ensureInstance()) { // No template found yet return; } } else if (this.restamp) { this.__teardownInstance(); } this._showHideChildren(); if ((!suppressTemplateNotifications || this.notifyDomChange) && this.if != this._lastIf) { this.dispatchEvent(new CustomEvent('dom-change', { bubbles: true, composed: true })); this._lastIf = this.if; } } // Ideally these would be annotated as abstract methods in an abstract class, // but closure compiler is finnicky /* eslint-disable valid-jsdoc */ /** * Abstract API to be implemented by subclass: Returns true if a template * instance has been created and inserted. * * @protected * @return {boolean} True when an instance has been created. */ __hasInstance() { } /** * Abstract API to be implemented by subclass: Returns the child nodes stamped * from a template instance. * * @protected * @return {Array<Node>} Array of child nodes stamped from the template * instance. */ __getInstanceNodes() { } /** * Abstract API to be implemented by subclass: Creates an instance of the * template and inserts it into the given parent node. * * @protected * @param {Node} parentNode The parent node to insert the instance into * @return {void} */ __createAndInsertInstance(parentNode) { } // eslint-disable-line no-unused-vars /** * Abstract API to be implemented by subclass: Removes nodes created by an * instance of a template and any associated cleanup. * * @protected * @return {void} */ __teardownInstance() { } /** * Abstract API to be implemented by subclass: Shows or hides any template * instance childNodes based on the `if` state of the element and its * `__hideTemplateChildren__` property. * * @protected * @return {void} */ _showHideChildren() { } /* eslint-enable valid-jsdoc */ } /** * The version of DomIf used when `fastDomIf` setting is in use, which is * optimized for first-render (but adds a tax to all subsequent property updates * on the host, whether they were used in a given `dom-if` or not). * * This implementation avoids use of `Templatizer`, which introduces a new scope * (a non-element PropertyEffects instance), which is not strictly necessary * since `dom-if` never introduces new properties to its scope (unlike * `dom-repeat`). Taking advantage of this fact, the `dom-if` reaches up to its * `__dataHost` and stamps the template directly from the host using the host's * runtime `_stampTemplate` API, which binds the property effects of the * template directly to the host. This both avoids the intermediary * `Templatizer` instance, but also avoids the need to bind host properties to * the `<template>` element and forward those into the template instance. * * In this version of `dom-if`, the `this.__instance` method is the * `DocumentFragment` returned from `_stampTemplate`, which also serves as the * handle for later removing it using the `_removeBoundDom` method. */ class DomIfFast extends DomIfBase { constructor() { super(); this.__instance = null; this.__syncInfo = null; } /** * Implementation of abstract API needed by DomIfBase. * * @override * @return {boolean} True when an instance has been created. */ __hasInstance() { return Boolean(this.__instance); } /** * Implementation of abstract API needed by DomIfBase. * * @override * @return {Array<Node>} Array of child nodes stamped from the template * instance. */ __getInstanceNodes() { return this.__instance.templateInfo.childNodes; } /** * Implementation of abstract API needed by DomIfBase. * * Stamps the template by calling `_stampTemplate` on the `__dataHost` of this * element and then inserts the resulting nodes into the given `parentNode`. * * @override * @param {Node} parentNode The parent node to insert the instance into * @return {void} */ __createAndInsertInstance(parentNode) { const host = this.__dataHost || this; if (strictTemplatePolicy) { if (!this.__dataHost) { throw new Error('strictTemplatePolicy: template owner not trusted'); } } // Pre-bind and link the template into the effects system const templateInfo = host._bindTemplate( /** @type {!HTMLTemplateElement} */ (this.__template), true); // Install runEffects hook that prevents running property effects // (and any nested template effects) when the `if` is false templateInfo.runEffects = (runEffects, changedProps, hasPaths) => { let syncInfo = this.__syncInfo; if (this.if) { // Mix any props that changed while the `if` was false into `changedProps` if (syncInfo) { // If there were properties received while the `if` was false, it is // important to sync the hidden state with the element _first_, so that // new bindings to e.g. `textContent` do not get stomped on by // pre-hidden values if `_showHideChildren` were to be called later at // the next render. Clearing `__invalidProps` here ensures // `_showHideChildren`'s call to `__syncHostProperties` no-ops, so // that we don't call `runEffects` more often than necessary. this.__syncInfo = null; this._showHideChildren(); changedProps = Object.assign(syncInfo.changedProps, changedProps); } runEffects(changedProps, hasPaths); } else { // Accumulate any values changed while `if` was false, along with the // runEffects method to sync them, so that we can replay them once `if` // becomes true if (this.__instance) { if (!syncInfo) { syncInfo = this.__syncInfo = { runEffects, changedProps: {} }; } if (hasPaths) { // Store root object of any paths; this will ensure direct bindings // like [[obj.foo]] bindings run after a `set('obj.foo', v)`, but // note that path notifications like `set('obj.foo.bar', v)` will // not propagate. Since batched path notifications are not // supported, we cannot simply accumulate path notifications. This // is equivalent to the non-fastDomIf case, which stores root(p) in // __invalidProps. for (const p in changedProps) { const rootProp = root(p); syncInfo.changedProps[rootProp] = this.__dataHost[rootProp]; } } else { Object.assign(syncInfo.changedProps, changedProps); } } } }; // Stamp the template, and set its DocumentFragment to the "instance" this.__instance = host._stampTemplate( /** @type {!HTMLTemplateElement} */ (this.__template), templateInfo); wrap(parentNode).insertBefore(this.__instance, this); } /** * Run effects for any properties that changed while the `if` was false. * * @return {void} */ __syncHostProperties() { const syncInfo = this.__syncInfo; if (syncInfo) { this.__syncInfo = null; syncInfo.runEffects(syncInfo.changedProps, false); } } /** * Implementation of abstract API needed by DomIfBase. * * Remove the instance and any nodes it created. Uses the `__dataHost`'s * runtime `_removeBoundDom` method. * * @override * @return {void} */ __teardownInstance() { const host = this.__dataHost || this; if (this.__instance) { host._removeBoundDom(this.__instance); this.__instance = null; this.__syncInfo = null; } } /** * Implementation of abstract API needed by DomIfBase. * * Shows or hides the template instance top level child nodes. For * text nodes, `textContent` is removed while "hidden" and replaced when * "shown." * * @override * @return {void} * @protected * @suppress {visibility} */ _showHideChildren() { const hidden = this.__hideTemplateChildren__ || !this.if; if (this.__instance && Boolean(this.__instance.__hidden) !== hidden) { this.__instance.__hidden = hidden; showHideChildren(hidden, this.__instance.templateInfo.childNodes); } if (!hidden) { this.__syncHostProperties(); } } } /** * The "legacy" implementation of `dom-if`, implemented using `Templatizer`. * * In this version, `this.__instance` is the `TemplateInstance` returned * from the templatized constructor. */ class DomIfLegacy extends DomIfBase { constructor() { super(); this.__ctor = null; this.__instance = null; this.__invalidProps = null; } /** * Implementation of abstract API needed by DomIfBase. * * @override * @return {boolean} True when an instance has been created. */ __hasInstance() { return Boolean(this.__instance); } /** * Implementation of abstract API needed by DomIfBase. * * @override * @return {Array<Node>} Array of child nodes stamped from the template * instance. */ __getInstanceNodes() { return this.__instance.children; } /** * Implementation of abstract API needed by DomIfBase. * * Stamps the template by creating a new instance of the templatized * constructor (which is created lazily if it does not yet exist), and then * inserts its resulting `root` doc fragment into the given `parentNode`. * * @override * @param {Node} parentNode The parent node to insert the instance into * @return {void} */ __createAndInsertInstance(parentNode) { // Ensure we have an instance constructor if (!this.__ctor) { this.__ctor = templatize( /** @type {!HTMLTemplateElement} */ (this.__template), this, { // dom-if templatizer instances require `mutable: true`, as // `__syncHostProperties` relies on that behavior to sync objects mutableData: true, /** * @param {string} prop Property to forward * @param {*} value Value of property * @this {DomIfLegacy} */ forwardHostProp: function(prop, value) { if (this.__instance) { if (this.if) { this.__instance.forwardHostProp(prop, value); } else { // If we have an instance but are squelching host property // forwarding due to if being false, note the invalidated // properties so `__syncHostProperties` can sync them the next // time `if` becomes true this.__invalidProps = this.__invalidProps || Object.create(null); this.__invalidProps[root(prop)] = true; } } } }); } // Create and insert the instance this.__instance = new this.__ctor(); wrap(parentNode).insertBefore(this.__instance.root, this); } /** * Implementation of abstract API needed by DomIfBase. * * Removes the instance and any nodes it created. * * @override * @return {void} */ __teardownInstance() { if (this.__instance) { let c$ = this.__instance.children; if (c$ && c$.length) { // use first child parent, for case when dom-if may have been detached let parent = wrap(c$[0]).parentNode; // Instance children may be disconnected from parents when dom-if // detaches if a tree was innerHTML'ed if (parent) { parent = wrap(parent); for (let i=0, n; (i<c$.length) && (n=c$[i]); i++) { parent.removeChild(n); } } } this.__invalidProps = null; this.__instance = null; } } /** * Forwards any properties that changed while the `if` was false into the * template instance and flushes it. * * @return {void} */ __syncHostProperties() { let props = this.__invalidProps; if (props) { this.__invalidProps = null; for (let prop in props) { this.__instance._setPendingProperty(prop, this.__dataHost[prop]); } this.__instance._flushProperties(); } } /** * Implementation of abstract API needed by DomIfBase. * * Shows or hides the template instance top level child elements. For * text nodes, `textContent` is removed while "hidden" and replaced when * "shown." * * @override * @protected * @return {void} * @suppress {visibility} */ _showHideChildren() { const hidden = this.__hideTemplateChildren__ || !this.if; if (this.__instance && Boolean(this.__instance.__hidden) !== hidden) { this.__instance.__hidden = hidden; this.__instance._showHideChildren(hidden); } if (!hidden) { this.__syncHostProperties(); } } } /** * The `<dom-if>` element will stamp a light-dom `<template>` child when * the `if` property becomes truthy, and the template can use Polymer * data-binding and declarative event features when used in the context of * a Polymer element's template. * * When `if` becomes falsy, the stamped content is hidden but not * removed from dom. When `if` subsequently becomes truthy again, the content * is simply re-shown. This approach is used due to its favorable performance * characteristics: the expense of creating template content is paid only * once and lazily. * * Set the `restamp` property to true to force the stamped content to be * created / destroyed when the `if` condition changes. * * @customElement * @polymer * @extends DomIfBase * @constructor * @summary Custom element that conditionally stamps and hides or removes * template content based on a boolean flag. */ export const DomIf = fastDomIf ? DomIfFast : DomIfLegacy; customElements.define(DomIf.is, DomIf);