@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
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 { 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);