UNPKG

22.6 kBJavaScriptView Raw
1/**
2@license
3Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9*/
10import { PolymerElement } from '../../polymer-element.js';
11
12import { Debouncer } from '../utils/debounce.js';
13import { enqueueDebouncer, flush } from '../utils/flush.js';
14import { microTask } from '../utils/async.js';
15import { root } from '../utils/path.js';
16import { wrap } from '../utils/wrap.js';
17import { hideElementsGlobally } from '../utils/hide-template-controls.js';
18import { fastDomIf, strictTemplatePolicy, suppressTemplateNotifications } from '../utils/settings.js';
19import { showHideChildren, templatize } from '../utils/templatize.js';
20
21/**
22 * @customElement
23 * @polymer
24 * @extends PolymerElement
25 * @summary Base class for dom-if element; subclassed into concrete
26 * implementation.
27 */
28class DomIfBase extends PolymerElement {
29
30 // Not needed to find template; can be removed once the analyzer
31 // can find the tag name from customElements.define call
32 static get is() { return 'dom-if'; }
33
34 static get template() { return null; }
35
36 static get properties() {
37
38 return {
39
40 /**
41 * Fired whenever DOM is added or removed/hidden by this template (by
42 * default, rendering occurs lazily). To force immediate rendering, call
43 * `render`.
44 *
45 * @event dom-change
46 */
47
48 /**
49 * A boolean indicating whether this template should stamp.
50 */
51 if: {
52 type: Boolean,
53 observer: '__debounceRender'
54 },
55
56 /**
57 * When true, elements will be removed from DOM and discarded when `if`
58 * becomes false and re-created and added back to the DOM when `if`
59 * becomes true. By default, stamped elements will be hidden but left
60 * in the DOM when `if` becomes false, which is generally results
61 * in better performance.
62 */
63 restamp: {
64 type: Boolean,
65 observer: '__debounceRender'
66 },
67
68 /**
69 * When the global `suppressTemplateNotifications` setting is used, setting
70 * `notifyDomChange: true` will enable firing `dom-change` events on this
71 * element.
72 */
73 notifyDomChange: {
74 type: Boolean
75 }
76 };
77
78 }
79
80 constructor() {
81 super();
82 this.__renderDebouncer = null;
83 this._lastIf = false;
84 this.__hideTemplateChildren__ = false;
85 /** @type {!HTMLTemplateElement|undefined} */
86 this.__template;
87 /** @type {!TemplateInfo|undefined} */
88 this._templateInfo;
89 }
90
91 __debounceRender() {
92 // Render is async for 2 reasons:
93 // 1. To eliminate dom creation trashing if user code thrashes `if` in the
94 // same turn. This was more common in 1.x where a compound computed
95 // property could result in the result changing multiple times, but is
96 // mitigated to a large extent by batched property processing in 2.x.
97 // 2. To avoid double object propagation when a bag including values bound
98 // to the `if` property as well as one or more hostProps could enqueue
99 // the <dom-if> to flush before the <template>'s host property
100 // forwarding. In that scenario creating an instance would result in
101 // the host props being set once, and then the enqueued changes on the
102 // template would set properties a second time, potentially causing an
103 // object to be set to an instance more than once. Creating the
104 // instance async from flushing data ensures this doesn't happen. If
105 // we wanted a sync option in the future, simply having <dom-if> flush
106 // (or clear) its template's pending host properties before creating
107 // the instance would also avoid the problem.
108 this.__renderDebouncer = Debouncer.debounce(
109 this.__renderDebouncer
110 , microTask
111 , () => this.__render());
112 enqueueDebouncer(this.__renderDebouncer);
113 }
114
115 /**
116 * @override
117 * @return {void}
118 */
119 disconnectedCallback() {
120 super.disconnectedCallback();
121 const parent = wrap(this).parentNode;
122 if (!parent || (parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE &&
123 !wrap(parent).host)) {
124 this.__teardownInstance();
125 }
126 }
127
128 /**
129 * @override
130 * @return {void}
131 */
132 connectedCallback() {
133 super.connectedCallback();
134 if (!hideElementsGlobally()) {
135 this.style.display = 'none';
136 }
137 if (this.if) {
138 this.__debounceRender();
139 }
140 }
141
142 /**
143 * Ensures a template has been assigned to `this.__template`. If it has not
144 * yet been, it querySelectors for it in its children and if it does not yet
145 * exist (e.g. in parser-generated case), opens a mutation observer and
146 * waits for it to appear (returns false if it has not yet been found,
147 * otherwise true). In the `removeNestedTemplates` case, the "template" will
148 * be the `dom-if` element itself.
149 *
150 * @return {boolean} True when a template has been found, false otherwise
151 */
152 __ensureTemplate() {
153 if (!this.__template) {
154 // When `removeNestedTemplates` is true, the "template" is the element
155 // itself, which has been given a `_templateInfo` property
156 const thisAsTemplate = /** @type {!HTMLTemplateElement} */ (
157 /** @type {!HTMLElement} */ (this));
158 let template = thisAsTemplate._templateInfo ?
159 thisAsTemplate :
160 /** @type {!HTMLTemplateElement} */
161 (wrap(thisAsTemplate).querySelector('template'));
162 if (!template) {
163 // Wait until childList changes and template should be there by then
164 let observer = new MutationObserver(() => {
165 if (wrap(this).querySelector('template')) {
166 observer.disconnect();
167 this.__render();
168 } else {
169 throw new Error('dom-if requires a <template> child');
170 }
171 });
172 observer.observe(this, {childList: true});
173 return false;
174 }
175 this.__template = template;
176 }
177 return true;
178 }
179
180 /**
181 * Ensures a an instance of the template has been created and inserted. This
182 * method may return false if the template has not yet been found or if
183 * there is no `parentNode` to insert the template into (in either case,
184 * connection or the template-finding mutation observer firing will queue
185 * another render, causing this method to be called again at a more
186 * appropriate time).
187 *
188 * Subclasses should implement the following methods called here:
189 * - `__hasInstance`
190 * - `__createAndInsertInstance`
191 * - `__getInstanceNodes`
192 *
193 * @return {boolean} True if the instance was created, false otherwise.
194 */
195 __ensureInstance() {
196 let parentNode = wrap(this).parentNode;
197 if (!this.__hasInstance()) {
198 // Guard against element being detached while render was queued
199 if (!parentNode) {
200 return false;
201 }
202 // Find the template (when false, there was no template yet)
203 if (!this.__ensureTemplate()) {
204 return false;
205 }
206 this.__createAndInsertInstance(parentNode);
207 } else {
208 // Move instance children if necessary
209 let children = this.__getInstanceNodes();
210 if (children && children.length) {
211 // Detect case where dom-if was re-attached in new position
212 let lastChild = wrap(this).previousSibling;
213 if (lastChild !== children[children.length-1]) {
214 for (let i=0, n; (i<children.length) && (n=children[i]); i++) {
215 wrap(parentNode).insertBefore(n, this);
216 }
217 }
218 }
219 }
220 return true;
221 }
222
223 /**
224 * Forces the element to render its content. Normally rendering is
225 * asynchronous to a provoking change. This is done for efficiency so
226 * that multiple changes trigger only a single render. The render method
227 * should be called if, for example, template rendering is required to
228 * validate application state.
229 *
230 * @return {void}
231 */
232 render() {
233 flush();
234 }
235
236 /**
237 * Performs the key rendering steps:
238 * 1. Ensure a template instance has been stamped (when true)
239 * 2. Remove the template instance (when false and restamp:true)
240 * 3. Sync the hidden state of the instance nodes with the if/restamp state
241 * 4. Fires the `dom-change` event when necessary
242 *
243 * @return {void}
244 */
245 __render() {
246 if (this.if) {
247 if (!this.__ensureInstance()) {
248 // No template found yet
249 return;
250 }
251 } else if (this.restamp) {
252 this.__teardownInstance();
253 }
254 this._showHideChildren();
255 if ((!suppressTemplateNotifications || this.notifyDomChange)
256 && this.if != this._lastIf) {
257 this.dispatchEvent(new CustomEvent('dom-change', {
258 bubbles: true,
259 composed: true
260 }));
261 this._lastIf = this.if;
262 }
263 }
264
265 // Ideally these would be annotated as abstract methods in an abstract class,
266 // but closure compiler is finnicky
267 /* eslint-disable valid-jsdoc */
268 /**
269 * Abstract API to be implemented by subclass: Returns true if a template
270 * instance has been created and inserted.
271 *
272 * @protected
273 * @return {boolean} True when an instance has been created.
274 */
275 __hasInstance() { }
276
277 /**
278 * Abstract API to be implemented by subclass: Returns the child nodes stamped
279 * from a template instance.
280 *
281 * @protected
282 * @return {Array<Node>} Array of child nodes stamped from the template
283 * instance.
284 */
285 __getInstanceNodes() { }
286
287 /**
288 * Abstract API to be implemented by subclass: Creates an instance of the
289 * template and inserts it into the given parent node.
290 *
291 * @protected
292 * @param {Node} parentNode The parent node to insert the instance into
293 * @return {void}
294 */
295 __createAndInsertInstance(parentNode) { } // eslint-disable-line no-unused-vars
296
297 /**
298 * Abstract API to be implemented by subclass: Removes nodes created by an
299 * instance of a template and any associated cleanup.
300 *
301 * @protected
302 * @return {void}
303 */
304 __teardownInstance() { }
305
306 /**
307 * Abstract API to be implemented by subclass: Shows or hides any template
308 * instance childNodes based on the `if` state of the element and its
309 * `__hideTemplateChildren__` property.
310 *
311 * @protected
312 * @return {void}
313 */
314 _showHideChildren() { }
315 /* eslint-enable valid-jsdoc */
316}
317
318/**
319 * The version of DomIf used when `fastDomIf` setting is in use, which is
320 * optimized for first-render (but adds a tax to all subsequent property updates
321 * on the host, whether they were used in a given `dom-if` or not).
322 *
323 * This implementation avoids use of `Templatizer`, which introduces a new scope
324 * (a non-element PropertyEffects instance), which is not strictly necessary
325 * since `dom-if` never introduces new properties to its scope (unlike
326 * `dom-repeat`). Taking advantage of this fact, the `dom-if` reaches up to its
327 * `__dataHost` and stamps the template directly from the host using the host's
328 * runtime `_stampTemplate` API, which binds the property effects of the
329 * template directly to the host. This both avoids the intermediary
330 * `Templatizer` instance, but also avoids the need to bind host properties to
331 * the `<template>` element and forward those into the template instance.
332 *
333 * In this version of `dom-if`, the `this.__instance` method is the
334 * `DocumentFragment` returned from `_stampTemplate`, which also serves as the
335 * handle for later removing it using the `_removeBoundDom` method.
336 */
337class DomIfFast extends DomIfBase {
338
339 constructor() {
340 super();
341 this.__instance = null;
342 this.__syncInfo = null;
343 }
344
345 /**
346 * Implementation of abstract API needed by DomIfBase.
347 *
348 * @override
349 * @return {boolean} True when an instance has been created.
350 */
351 __hasInstance() {
352 return Boolean(this.__instance);
353 }
354
355 /**
356 * Implementation of abstract API needed by DomIfBase.
357 *
358 * @override
359 * @return {Array<Node>} Array of child nodes stamped from the template
360 * instance.
361 */
362 __getInstanceNodes() {
363 return this.__instance.templateInfo.childNodes;
364 }
365
366 /**
367 * Implementation of abstract API needed by DomIfBase.
368 *
369 * Stamps the template by calling `_stampTemplate` on the `__dataHost` of this
370 * element and then inserts the resulting nodes into the given `parentNode`.
371 *
372 * @override
373 * @param {Node} parentNode The parent node to insert the instance into
374 * @return {void}
375 */
376 __createAndInsertInstance(parentNode) {
377 const host = this.__dataHost || this;
378 if (strictTemplatePolicy) {
379 if (!this.__dataHost) {
380 throw new Error('strictTemplatePolicy: template owner not trusted');
381 }
382 }
383 // Pre-bind and link the template into the effects system
384 const templateInfo = host._bindTemplate(
385 /** @type {!HTMLTemplateElement} */ (this.__template), true);
386 // Install runEffects hook that prevents running property effects
387 // (and any nested template effects) when the `if` is false
388 templateInfo.runEffects = (runEffects, changedProps, hasPaths) => {
389 let syncInfo = this.__syncInfo;
390 if (this.if) {
391 // Mix any props that changed while the `if` was false into `changedProps`
392 if (syncInfo) {
393 // If there were properties received while the `if` was false, it is
394 // important to sync the hidden state with the element _first_, so that
395 // new bindings to e.g. `textContent` do not get stomped on by
396 // pre-hidden values if `_showHideChildren` were to be called later at
397 // the next render. Clearing `__invalidProps` here ensures
398 // `_showHideChildren`'s call to `__syncHostProperties` no-ops, so
399 // that we don't call `runEffects` more often than necessary.
400 this.__syncInfo = null;
401 this._showHideChildren();
402 changedProps = Object.assign(syncInfo.changedProps, changedProps);
403 }
404 runEffects(changedProps, hasPaths);
405 } else {
406 // Accumulate any values changed while `if` was false, along with the
407 // runEffects method to sync them, so that we can replay them once `if`
408 // becomes true
409 if (this.__instance) {
410 if (!syncInfo) {
411 syncInfo = this.__syncInfo = { runEffects, changedProps: {} };
412 }
413 if (hasPaths) {
414 // Store root object of any paths; this will ensure direct bindings
415 // like [[obj.foo]] bindings run after a `set('obj.foo', v)`, but
416 // note that path notifications like `set('obj.foo.bar', v)` will
417 // not propagate. Since batched path notifications are not
418 // supported, we cannot simply accumulate path notifications. This
419 // is equivalent to the non-fastDomIf case, which stores root(p) in
420 // __invalidProps.
421 for (const p in changedProps) {
422 const rootProp = root(p);
423 syncInfo.changedProps[rootProp] = this.__dataHost[rootProp];
424 }
425 } else {
426 Object.assign(syncInfo.changedProps, changedProps);
427 }
428 }
429 }
430 };
431 // Stamp the template, and set its DocumentFragment to the "instance"
432 this.__instance = host._stampTemplate(
433 /** @type {!HTMLTemplateElement} */ (this.__template), templateInfo);
434 wrap(parentNode).insertBefore(this.__instance, this);
435 }
436
437 /**
438 * Run effects for any properties that changed while the `if` was false.
439 *
440 * @return {void}
441 */
442 __syncHostProperties() {
443 const syncInfo = this.__syncInfo;
444 if (syncInfo) {
445 this.__syncInfo = null;
446 syncInfo.runEffects(syncInfo.changedProps, false);
447 }
448 }
449
450 /**
451 * Implementation of abstract API needed by DomIfBase.
452 *
453 * Remove the instance and any nodes it created. Uses the `__dataHost`'s
454 * runtime `_removeBoundDom` method.
455 *
456 * @override
457 * @return {void}
458 */
459 __teardownInstance() {
460 const host = this.__dataHost || this;
461 if (this.__instance) {
462 host._removeBoundDom(this.__instance);
463 this.__instance = null;
464 this.__syncInfo = null;
465 }
466 }
467
468 /**
469 * Implementation of abstract API needed by DomIfBase.
470 *
471 * Shows or hides the template instance top level child nodes. For
472 * text nodes, `textContent` is removed while "hidden" and replaced when
473 * "shown."
474 *
475 * @override
476 * @return {void}
477 * @protected
478 * @suppress {visibility}
479 */
480 _showHideChildren() {
481 const hidden = this.__hideTemplateChildren__ || !this.if;
482 if (this.__instance && Boolean(this.__instance.__hidden) !== hidden) {
483 this.__instance.__hidden = hidden;
484 showHideChildren(hidden, this.__instance.templateInfo.childNodes);
485 }
486 if (!hidden) {
487 this.__syncHostProperties();
488 }
489 }
490}
491
492/**
493 * The "legacy" implementation of `dom-if`, implemented using `Templatizer`.
494 *
495 * In this version, `this.__instance` is the `TemplateInstance` returned
496 * from the templatized constructor.
497 */
498class DomIfLegacy extends DomIfBase {
499
500 constructor() {
501 super();
502 this.__ctor = null;
503 this.__instance = null;
504 this.__invalidProps = null;
505 }
506
507 /**
508 * Implementation of abstract API needed by DomIfBase.
509 *
510 * @override
511 * @return {boolean} True when an instance has been created.
512 */
513 __hasInstance() {
514 return Boolean(this.__instance);
515 }
516
517 /**
518 * Implementation of abstract API needed by DomIfBase.
519 *
520 * @override
521 * @return {Array<Node>} Array of child nodes stamped from the template
522 * instance.
523 */
524 __getInstanceNodes() {
525 return this.__instance.children;
526 }
527
528 /**
529 * Implementation of abstract API needed by DomIfBase.
530 *
531 * Stamps the template by creating a new instance of the templatized
532 * constructor (which is created lazily if it does not yet exist), and then
533 * inserts its resulting `root` doc fragment into the given `parentNode`.
534 *
535 * @override
536 * @param {Node} parentNode The parent node to insert the instance into
537 * @return {void}
538 */
539 __createAndInsertInstance(parentNode) {
540 // Ensure we have an instance constructor
541 if (!this.__ctor) {
542 this.__ctor = templatize(
543 /** @type {!HTMLTemplateElement} */ (this.__template), this, {
544 // dom-if templatizer instances require `mutable: true`, as
545 // `__syncHostProperties` relies on that behavior to sync objects
546 mutableData: true,
547 /**
548 * @param {string} prop Property to forward
549 * @param {*} value Value of property
550 * @this {DomIfLegacy}
551 */
552 forwardHostProp: function(prop, value) {
553 if (this.__instance) {
554 if (this.if) {
555 this.__instance.forwardHostProp(prop, value);
556 } else {
557 // If we have an instance but are squelching host property
558 // forwarding due to if being false, note the invalidated
559 // properties so `__syncHostProperties` can sync them the next
560 // time `if` becomes true
561 this.__invalidProps =
562 this.__invalidProps || Object.create(null);
563 this.__invalidProps[root(prop)] = true;
564 }
565 }
566 }
567 });
568 }
569 // Create and insert the instance
570 this.__instance = new this.__ctor();
571 wrap(parentNode).insertBefore(this.__instance.root, this);
572 }
573
574 /**
575 * Implementation of abstract API needed by DomIfBase.
576 *
577 * Removes the instance and any nodes it created.
578 *
579 * @override
580 * @return {void}
581 */
582 __teardownInstance() {
583 if (this.__instance) {
584 let c$ = this.__instance.children;
585 if (c$ && c$.length) {
586 // use first child parent, for case when dom-if may have been detached
587 let parent = wrap(c$[0]).parentNode;
588 // Instance children may be disconnected from parents when dom-if
589 // detaches if a tree was innerHTML'ed
590 if (parent) {
591 parent = wrap(parent);
592 for (let i=0, n; (i<c$.length) && (n=c$[i]); i++) {
593 parent.removeChild(n);
594 }
595 }
596 }
597 this.__invalidProps = null;
598 this.__instance = null;
599 }
600 }
601
602 /**
603 * Forwards any properties that changed while the `if` was false into the
604 * template instance and flushes it.
605 *
606 * @return {void}
607 */
608 __syncHostProperties() {
609 let props = this.__invalidProps;
610 if (props) {
611 this.__invalidProps = null;
612 for (let prop in props) {
613 this.__instance._setPendingProperty(prop, this.__dataHost[prop]);
614 }
615 this.__instance._flushProperties();
616 }
617 }
618
619 /**
620 * Implementation of abstract API needed by DomIfBase.
621 *
622 * Shows or hides the template instance top level child elements. For
623 * text nodes, `textContent` is removed while "hidden" and replaced when
624 * "shown."
625 *
626 * @override
627 * @protected
628 * @return {void}
629 * @suppress {visibility}
630 */
631 _showHideChildren() {
632 const hidden = this.__hideTemplateChildren__ || !this.if;
633 if (this.__instance && Boolean(this.__instance.__hidden) !== hidden) {
634 this.__instance.__hidden = hidden;
635 this.__instance._showHideChildren(hidden);
636 }
637 if (!hidden) {
638 this.__syncHostProperties();
639 }
640 }
641}
642
643/**
644 * The `<dom-if>` element will stamp a light-dom `<template>` child when
645 * the `if` property becomes truthy, and the template can use Polymer
646 * data-binding and declarative event features when used in the context of
647 * a Polymer element's template.
648 *
649 * When `if` becomes falsy, the stamped content is hidden but not
650 * removed from dom. When `if` subsequently becomes truthy again, the content
651 * is simply re-shown. This approach is used due to its favorable performance
652 * characteristics: the expense of creating template content is paid only
653 * once and lazily.
654 *
655 * Set the `restamp` property to true to force the stamped content to be
656 * created / destroyed when the `if` condition changes.
657 *
658 * @customElement
659 * @polymer
660 * @extends DomIfBase
661 * @constructor
662 * @summary Custom element that conditionally stamps and hides or removes
663 * template content based on a boolean flag.
664 */
665export const DomIf = fastDomIf ? DomIfFast : DomIfLegacy;
666
667customElements.define(DomIf.is, DomIf);