Merge pull request #5560 from Polymer/legacy-undefined-noBatch-altTemplates

Optimizations to template elements - fastDomIf & removeNestedTemplates
This commit is contained in:
Kevin Schaaf
2019-07-11 16:48:45 -07:00
committed by GitHub
12 changed files with 815 additions and 237 deletions
+454 -97
View File
@@ -16,29 +16,17 @@ 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 } from '../utils/settings.js';
import { showHideChildren } from '../utils/templatize.js';
/**
* 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 PolymerElement
* @summary Custom element that conditionally stamps and hides or removes
* template content based on a boolean flag.
* @summary Base class for dom-if element; subclassed into concrete
* implementation.
*/
export class DomIf extends PolymerElement {
class DomIfBase extends PolymerElement {
// Not needed to find template; can be removed once the analyzer
// can find the tag name from customElements.define call
@@ -85,11 +73,12 @@ export class DomIf extends PolymerElement {
constructor() {
super();
this.__renderDebouncer = null;
this.__invalidProps = null;
this.__instance = null;
this._lastIf = false;
this.__ctor = null;
this.__hideTemplateChildren__ = false;
/** @type {!HTMLTemplateElement|undefined} */
this.__template;
/** @type {!TemplateInfo|undefined} */
this._templateInfo;
}
__debounceRender() {
@@ -143,31 +132,119 @@ export class DomIf extends PolymerElement {
}
}
/**
* 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;
}
this._showHideChildren();
} else if (this.restamp) {
this.__teardownInstance();
}
if (!this.restamp && this.__instance) {
this._showHideChildren();
}
this._showHideChildren();
if (this.if != this._lastIf) {
this.dispatchEvent(new CustomEvent('dom-change', {
bubbles: true,
@@ -177,81 +254,313 @@ export class DomIf extends PolymerElement {
}
}
__ensureInstance() {
let parentNode = wrap(this).parentNode;
// Guard against element being detached while render was queued
if (parentNode) {
if (!this.__ctor) {
let template = /** @type {HTMLTemplateElement} */(wrap(this).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;
// 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) => {
const 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);
hasPaths = hasPaths || syncInfo.hasPaths;
}
this.__ctor = templatize(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 {DomIf}
*/
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;
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 (syncInfo) {
syncInfo.hasPaths = syncInfo.hasPaths || hasPaths;
Object.assign(syncInfo.changedProps, changedProps);
} else {
this.__syncInfo = {
runEffects,
changedProps: Object.assign({}, changedProps),
hasPaths
};
}
}
};
// 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, syncInfo.hasPaths);
}
}
/**
* 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;
}
}
/**
* 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;
}
}
}
}
});
}
if (!this.__instance) {
this.__instance = new this.__ctor();
wrap(parentNode).insertBefore(this.__instance.root, this);
} else {
this.__syncHostProperties();
let c$ = this.__instance.children;
if (c$ && c$.length) {
// Detect case where dom-if was re-attached in new position
let lastChild = wrap(this).previousSibling;
if (lastChild !== c$[c$.length-1]) {
for (let i=0, n; (i<c$.length) && (n=c$[i]); i++) {
wrap(parentNode).insertBefore(n, this);
}
}
}
}
}
return true;
}
__syncHostProperties() {
let props = this.__invalidProps;
if (props) {
for (let prop in props) {
this.__instance._setPendingProperty(prop, this.__dataHost[prop]);
}
this.__invalidProps = null;
this.__instance._flushProperties();
});
}
// 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;
@@ -267,26 +576,74 @@ export class DomIf extends PolymerElement {
}
}
}
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) {
for (let prop in props) {
this.__instance._setPendingProperty(prop, this.__dataHost[prop]);
}
this.__instance._flushProperties();
this.__invalidProps = null;
}
}
/**
* 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."
* @return {void}
*
* @override
* @protected
* @return {void}
* @suppress {visibility}
*/
_showHideChildren() {
let hidden = this.__hideTemplateChildren__ || !this.if;
if (this.__instance) {
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);
+10 -2
View File
@@ -301,6 +301,8 @@ export class DomRepeat extends domRepeatBase {
this.__ctor = null;
this.__isDetached = true;
this.template = null;
/** @type {TemplateInfo} */
this._templateInfo;
}
/**
@@ -339,9 +341,15 @@ export class DomRepeat extends domRepeatBase {
// until ready, since won't have its template content handed back to
// it until then
if (!this.__ctor) {
let template = this.template = /** @type {HTMLTemplateElement} */(this.querySelector('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 = this.template = thisAsTemplate._templateInfo ?
thisAsTemplate :
/** @type {!HTMLTemplateElement} */ (this.querySelector('template'));
if (!template) {
// // Wait until childList changes and template should be there by then
// Wait until childList changes and template should be there by then
let observer = new MutationObserver(() => {
if (this.querySelector('template')) {
observer.disconnect();
+119 -42
View File
@@ -19,7 +19,7 @@ import { camelToDashCase, dashToCamelCase } from '../utils/case-map.js';
import { PropertyAccessors } from './property-accessors.js';
/* for annotated effects */
import { TemplateStamp } from './template-stamp.js';
import { sanitizeDOMValue, legacyUndefined, legacyNoBatch, legacyNotifyOrder, orderedComputed } from '../utils/settings.js';
import { sanitizeDOMValue, legacyUndefined, legacyNoBatch, legacyNotifyOrder, orderedComputed, removeNestedTemplates, fastDomIf } from '../utils/settings.js';
// Monotonically increasing unique ID used for de-duping effects triggered
// from multiple properties in the same turn
@@ -1285,9 +1285,6 @@ function upper(name) {
* @appliesMixin PropertyAccessors
* @summary Element class mixin that provides meta-programming for Polymer's
* template binding and data observation system.
* @template T
* @param {function(new:T)} superClass Class to apply mixin to.
* @return {function(new:T)} superClass with mixin applied.
*/
export const PropertyEffects = dedupingMixin(superClass => {
@@ -1361,9 +1358,6 @@ export const PropertyEffects = dedupingMixin(superClass => {
this.__templateInfo;
}
/**
* @return {!Object<string, string>} Effect prototype property name map.
*/
get PROPERTY_EFFECT_TYPES() {
return TYPES;
}
@@ -1989,11 +1983,23 @@ export const PropertyEffects = dedupingMixin(superClass => {
if (this[TYPES.PROPAGATE]) {
runEffects(this, this[TYPES.PROPAGATE], changedProps, oldProps, hasPaths);
}
let templateInfo = this.__templateInfo;
while (templateInfo) {
if (this.__templateInfo) {
this._runEffectsForTemplate(this.__templateInfo, changedProps, oldProps, hasPaths);
}
}
_runEffectsForTemplate(templateInfo, changedProps, oldProps, hasPaths) {
const baseRunEffects = (changedProps, hasPaths) => {
runEffects(this, templateInfo.propertyEffects, changedProps, oldProps,
hasPaths, templateInfo.nodeList);
templateInfo = templateInfo.nextTemplateInfo;
for (let info=templateInfo.firstChild; info; info=info.nextSibling) {
this._runEffectsForTemplate(info, changedProps, oldProps, hasPaths);
}
};
if (templateInfo.runEffects) {
templateInfo.runEffects(baseRunEffects, changedProps, hasPaths);
} else {
baseRunEffects(changedProps, hasPaths);
}
}
@@ -2686,7 +2692,7 @@ export const PropertyEffects = dedupingMixin(superClass => {
*/
_bindTemplate(template, instanceBinding) {
let templateInfo = this.constructor._parseTemplate(template);
let wasPreBound = this.__templateInfo == templateInfo;
let wasPreBound = this.__preBoundTemplateInfo == templateInfo;
// Optimization: since this is called twice for proto-bound templates,
// don't attempt to recreate accessors if this template was pre-bound
if (!wasPreBound) {
@@ -2696,17 +2702,39 @@ export const PropertyEffects = dedupingMixin(superClass => {
}
if (instanceBinding) {
// For instance-time binding, create instance of template metadata
// and link into list of templates if necessary
// and link into tree of templates if necessary
templateInfo = /** @type {!TemplateInfo} */(Object.create(templateInfo));
templateInfo.wasPreBound = wasPreBound;
if (!wasPreBound && this.__templateInfo) {
let last = this.__templateInfoLast || this.__templateInfo;
this.__templateInfoLast = last.nextTemplateInfo = templateInfo;
templateInfo.previousTemplateInfo = last;
return templateInfo;
if (!this.__templateInfo) {
// Set the info to the root of the tree
this.__templateInfo = templateInfo;
} else {
// Append this template info onto the end of its parent template's
// list, which will determine the tree structure via which property
// effects are run; if this template was not nested in another
// template, use the root template (the first stamped one) as the
// parent. Note, `parent` is the `templateInfo` instance for this
// template's parent (containing) template, which was set up in
// `applyTemplateContent`. While a given template's `parent` is set
// apriori, it is only added to the parent's child list at the point
// that it is being bound, since a template may or may not ever be
// stamped, and may be stamped more than once (in which case instances
// of the template info will be in the tree under its parent more than
// once).
const parent = templateInfo.parent || this.__templateInfo;
const previous = parent.lastChild;
parent.lastChild = templateInfo;
templateInfo.previousSibling = previous;
if (previous) {
previous.nextSibling = templateInfo;
} else {
parent.firstChild = templateInfo;
}
}
} else {
this.__preBoundTemplateInfo = templateInfo;
}
return this.__templateInfo = templateInfo;
return templateInfo;
}
/**
@@ -2747,17 +2775,20 @@ export const PropertyEffects = dedupingMixin(superClass => {
* in the main element template.
*
* @param {!HTMLTemplateElement} template Template to stamp
* @param {TemplateInfo=} templateInfo Optional bound template info associated
* with the template to be stamped; if omitted the template will be
* automatically bound.
* @return {!StampedTemplate} Cloned template content
* @override
* @protected
*/
_stampTemplate(template) {
_stampTemplate(template, templateInfo) {
templateInfo = templateInfo || /** @type {!TemplateInfo} */(this._bindTemplate(template, true));
// Ensures that created dom is `_enqueueClient`'d to this element so
// that it can be flushed on next call to `_flushProperties`
hostStack.beginHosting(this);
let dom = super._stampTemplate(template);
let dom = super._stampTemplate(template, templateInfo);
hostStack.endHosting(this);
let templateInfo = /** @type {!TemplateInfo} */(this._bindTemplate(template, true));
// Add template-instance-specific data to instanced templateInfo
templateInfo.nodeList = dom.nodeList;
// Capture child nodes to allow unstamping of non-prototypical templates
@@ -2770,10 +2801,17 @@ export const PropertyEffects = dedupingMixin(superClass => {
dom.templateInfo = templateInfo;
// Setup compound storage, 2-way listeners, and dataHost for bindings
setupBindings(this, templateInfo);
// Flush properties into template nodes if already booted
if (this.__dataReady) {
runEffects(this, templateInfo.propertyEffects, this.__data, null,
false, templateInfo.nodeList);
// Flush properties into template nodes; the check on `__dataClientsReady`
// ensures we don't needlessly run effects for an element's initial
// prototypical template stamping since they will happen as a part of the
// first call to `_propertiesChanged`. This flag is set to true
// after running the initial propagate effects, and immediately before
// flushing clients. Since downstream clients could cause stamping on
// this host (e.g. a fastDomIf `dom-if` being forced to render
// synchronously), this flag ensures effects for runtime-stamped templates
// are run at this point during the initial element boot-up.
if (this.__dataClientsReady) {
this._runEffectsForTemplate(templateInfo, this.__data, null, false);
}
return dom;
}
@@ -2789,25 +2827,27 @@ export const PropertyEffects = dedupingMixin(superClass => {
* @protected
*/
_removeBoundDom(dom) {
// Unlink template info
// Unlink template info; Note that while the child is unlinked from its
// parent list, a template's `parent` reference is never removed, since
// this is is determined by the tree structure and applied at
// `applyTemplateContent` time.
let templateInfo = dom.templateInfo;
if (templateInfo.previousTemplateInfo) {
templateInfo.previousTemplateInfo.nextTemplateInfo =
templateInfo.nextTemplateInfo;
const {previousSibling, nextSibling, parent} = templateInfo;
if (previousSibling) {
previousSibling.nextSibling = nextSibling;
} else if (parent) {
parent.firstChild = nextSibling;
}
if (templateInfo.nextTemplateInfo) {
templateInfo.nextTemplateInfo.previousTemplateInfo =
templateInfo.previousTemplateInfo;
if (nextSibling) {
nextSibling.previousSibling = previousSibling;
} else if (parent) {
parent.lastChild = previousSibling;
}
if (this.__templateInfoLast == templateInfo) {
this.__templateInfoLast = templateInfo.previousTemplateInfo;
}
templateInfo.previousTemplateInfo = templateInfo.nextTemplateInfo = null;
// Remove stamped nodes
let nodes = templateInfo.childNodes;
for (let i=0; i<nodes.length; i++) {
let node = nodes[i];
node.parentNode.removeChild(node);
wrap(wrap(node).parentNode).removeChild(node);
}
}
@@ -2936,12 +2976,49 @@ export const PropertyEffects = dedupingMixin(superClass => {
// Change back to just super.methodCall()
let noted = propertyEffectsBase._parseTemplateNestedTemplate.call(
this, node, templateInfo, nodeInfo);
const parent = node.parentNode;
const nestedTemplateInfo = nodeInfo.templateInfo;
const isDomIf = parent.localName === 'dom-if';
const isDomRepeat = parent.localName === 'dom-repeat';
// Remove nested template and redirect its host bindings & templateInfo
// onto the parent (dom-if/repeat element)'s nodeInfo
if (removeNestedTemplates && (isDomIf || isDomRepeat)) {
parent.removeChild(node);
// Use the parent's nodeInfo (for the dom-if/repeat) to record the
// templateInfo, and use that for any host property bindings below
nodeInfo = nodeInfo.parentInfo;
nodeInfo.templateInfo = nestedTemplateInfo;
// Ensure the parent dom-if/repeat is noted since it now may have host
// bindings; it may not have been if it did not have its own bindings
nodeInfo.noted = true;
noted = false;
}
// Merge host props into outer template and add bindings
let hostProps = nodeInfo.templateInfo.hostProps;
let mode = '{';
for (let source in hostProps) {
let parts = [{ mode, source, dependencies: [source], hostProp: true }];
addBinding(this, templateInfo, nodeInfo, 'property', '_host_' + source, parts);
let hostProps = nestedTemplateInfo.hostProps;
if (fastDomIf && isDomIf) {
// `fastDomIf` mode uses runtime-template stamping to add accessors/
// effects to properties used in its template; as such we don't need to
// tax the host element with `_host_` bindings for the `dom-if`.
// However, in the event it is nested in a `dom-repeat`, it is still
// important that its host properties are added to the
// TemplateInstance's `hostProps` so that they are forwarded to the
// TemplateInstance.
if (hostProps) {
templateInfo.hostProps =
Object.assign(templateInfo.hostProps || {}, hostProps);
// Ensure the dom-if is noted so that it has a __dataHost, since
// `fastDomIf` uses the host for runtime template stamping; note this
// was already ensured above in the `removeNestedTemplates` case
if (!removeNestedTemplates) {
nodeInfo.parentInfo.noted = true;
}
}
} else {
let mode = '{';
for (let source in hostProps) {
let parts = [{ mode, source, dependencies: [source], hostProp: true }];
addBinding(this, templateInfo, nodeInfo, 'property', '_host_' + source, parts);
}
}
return noted;
}
+18 -9
View File
@@ -72,9 +72,11 @@ function applyEventListener(inst, node, nodeInfo) {
}
// push configuration references at configure time
function applyTemplateContent(inst, node, nodeInfo) {
function applyTemplateContent(inst, node, nodeInfo, parentTemplateInfo) {
if (nodeInfo.templateInfo) {
node._templateInfo = nodeInfo.templateInfo;
// Give the node an instance of this templateInfo and set its parent
node._templateInfo = Object.create(nodeInfo.templateInfo);
node._templateInfo.parent = parentTemplateInfo;
}
}
@@ -104,9 +106,6 @@ function createNodeEventHandler(context, eventName, methodName) {
* @mixinFunction
* @polymer
* @summary Element class mixin that provides basic template parsing and stamping
* @template T
* @param {function(new:T)} superClass Class to apply mixin to.
* @return {function(new:T)} superClass with mixin applied.
*/
export const TemplateStamp = dedupingMixin(
/**
@@ -258,7 +257,11 @@ export const TemplateStamp = dedupingMixin(
if (element.hasAttributes && element.hasAttributes()) {
noted = this._parseTemplateNodeAttributes(element, templateInfo, nodeInfo) || noted;
}
return noted;
// Checking `nodeInfo.noted` allows a child node of this node (who gets
// access to `parentInfo`) to cause the parent to be noted, which
// otherwise has no return path via `_parseTemplateChildNodes` (used by
// some optimizations)
return noted || nodeInfo.noted;
}
/**
@@ -437,16 +440,22 @@ export const TemplateStamp = dedupingMixin(
* is removed and stored in notes as well.
*
* @param {!HTMLTemplateElement} template Template to stamp
* @param {TemplateInfo=} templateInfo Optional template info associated
* with the template to be stamped; if omitted the template will be
* automatically parsed.
* @return {!StampedTemplate} Cloned template content
* @override
*/
_stampTemplate(template) {
_stampTemplate(template, templateInfo) {
// Polyfill support: bootstrap the template if it has not already been
if (template && !template.content &&
window.HTMLTemplateElement && HTMLTemplateElement.decorate) {
HTMLTemplateElement.decorate(template);
}
let templateInfo = this.constructor._parseTemplate(template);
// Accepting the `templateInfo` via an argument allows for creating
// instances of the `templateInfo` by the caller, useful for adding
// instance-time information to the prototypical data
templateInfo = templateInfo || this.constructor._parseTemplate(template);
let nodeInfo = templateInfo.nodeInfoList;
let content = templateInfo.content || template.content;
let dom = /** @type {DocumentFragment} */ (document.importNode(content, true));
@@ -457,7 +466,7 @@ export const TemplateStamp = dedupingMixin(
for (let i=0, l=nodeInfo.length, info; (i<l) && (info=nodeInfo[i]); i++) {
let node = nodes[i] = findTemplateNode(dom, info);
applyIdToMap(this, dom.$, node, info);
applyTemplateContent(this, node, info);
applyTemplateContent(this, node, info, templateInfo);
applyEventListener(this, node, info);
}
dom = /** @type {!StampedTemplate} */(dom); // eslint-disable-line no-self-assign
+65 -12
View File
@@ -23,7 +23,8 @@ export const useNativeCustomElements = !(window.customElements.polyfillWrapFlush
* `rootPath` to provide a stable application mount path when
* using client side routing.
*/
export let rootPath = pathFromUrl(document.baseURI || window.location.href);
export let rootPath = window.Polymer && window.Polymer.rootPath ||
pathFromUrl(document.baseURI || window.location.href);
/**
* Sets the global rootPath property used by `ElementMixin` and
@@ -51,7 +52,8 @@ export const setRootPath = function(path) {
*
* @type {(function(*,string,string,Node):*)|undefined}
*/
export let sanitizeDOMValue = window.Polymer && window.Polymer.sanitizeDOMValue || undefined;
export let sanitizeDOMValue =
window.Polymer && window.Polymer.sanitizeDOMValue || undefined;
/**
* Sets the global sanitizeDOMValue available via this module's exported
@@ -70,7 +72,8 @@ export const setSanitizeDOMValue = function(newSanitizeDOMValue) {
* scrolling performance.
* Defaults to `false` for backwards compatibility.
*/
export let passiveTouchGestures = false;
export let passiveTouchGestures =
window.Polymer && window.Polymer.setPassiveTouchGestures || false;
/**
* Sets `passiveTouchGestures` globally for all elements using Polymer Gestures.
@@ -88,7 +91,8 @@ export const setPassiveTouchGestures = function(usePassive) {
* disallowed, `<dom-bind>` is disabled, and `<dom-if>`/`<dom-repeat>`
* templates will only evaluate in the context of a trusted element template.
*/
export let strictTemplatePolicy = false;
export let strictTemplatePolicy =
window.Polymer && window.Polymer.strictTemplatePolicy || false;
/**
* Sets `strictTemplatePolicy` globally for all elements
@@ -107,7 +111,8 @@ export const setStrictTemplatePolicy = function(useStrictPolicy) {
* getter and the `html` tag function. To enable legacy loading of templates
* via dom-module, set this flag to true.
*/
export let allowTemplateFromDomModule = false;
export let allowTemplateFromDomModule =
window.Polymer && window.Polymer.allowTemplateFromDomModule || false;
/**
* Sets `lookupTemplateFromDomModule` globally for all elements
@@ -127,7 +132,8 @@ export const setAllowTemplateFromDomModule = function(allowDomModule) {
* If no includes or relative urls are used in styles, these steps can be
* skipped as an optimization.
*/
export let legacyOptimizations = false;
export let legacyOptimizations =
window.Polymer && window.Polymer.legacyOptimizations || false;
/**
* Sets `legacyOptimizations` globally for all elements to enable optimizations
@@ -144,7 +150,8 @@ export const setLegacyOptimizations = function(useLegacyOptimizations) {
/**
* Setting to add warnings useful when migrating from Polymer 1.x to 2.x.
*/
export let legacyWarnings = false;
export let legacyWarnings =
window.Polymer && window.Polymer.legacyWarnings || false;
/**
* Sets `legacyWarnings` globally for all elements to migration warnings.
@@ -160,7 +167,8 @@ export const setLegacyWarnings = function(useLegacyWarnings) {
* Setting to perform initial rendering synchronously when running under ShadyDOM.
* This matches the behavior of Polymer 1.
*/
export let syncInitialRender = false;
export let syncInitialRender =
window.Polymer && window.Polymer.syncInitialRender || false;
/**
* Sets `syncInitialRender` globally for all elements to enable synchronous
@@ -179,7 +187,8 @@ export const setSyncInitialRender = function(useSyncInitialRender) {
* observers around undefined values. Observers and computed property methods
* are not called until no argument is undefined.
*/
export let legacyUndefined = false;
export let legacyUndefined =
window.Polymer && window.Polymer.legacyUndefined || false;
/**
* Sets `legacyUndefined` globally for all elements to enable legacy
@@ -197,7 +206,8 @@ export const setLegacyUndefined = function(useLegacyUndefined) {
* Setting to retain the legacy Polymer 1 behavior for setting properties. Does
* not batch property sets.
*/
export let legacyNoBatch = false;
export let legacyNoBatch =
window.Polymer && window.Polymer.legacyNoBatch || false;
/**
* Sets `legacyNoBatch` globally for all elements to enable legacy
@@ -216,7 +226,8 @@ export const setLegacyNoBatch = function(useLegacyNoBatch) {
* fire change events with respect to other effects. In Polymer 1.x they fire
* before observers; in 2.x they fire after all other effect types.
*/
export let legacyNotifyOrder = false;
export let legacyNotifyOrder =
window.Polymer && window.Polymer.legacyNotifyOrder || false;
/**
* Sets `legacyNotifyOrder` globally for all elements to enable legacy
@@ -233,7 +244,8 @@ export const setLegacyNotifyOrder = function(useLegacyNotifyOrder) {
* Setting to ensure computed properties are computed in order to ensure
* re-computation never occurs in a given turn.
*/
export let orderedComputed = false;
export let orderedComputed =
window.Polymer && window.Polymer.orderedComputed || false;
/**
* Sets `orderedComputed` globally for all elements to enable ordered computed
@@ -263,3 +275,44 @@ export let cancelSyntheticClickEvents = true;
export const setCancelSyntheticClickEvents = function(useCancelSyntheticClickEvents) {
cancelSyntheticClickEvents = useCancelSyntheticClickEvents;
};
/**
* Setting to remove nested templates inside `dom-if` and `dom-repeat` as
* part of element template parsing. This is a performance optimization that
* eliminates most of the tax of needing two elements due to the loss of
* type-extended templates as a result of the V1 specification changes.
*/
export let removeNestedTemplates =
window.Polymer && window.Polymer.removeNestedTemplates || false;
/**
* Sets `removeNestedTemplates` globally, to eliminate nested templates
* inside `dom-if` and `dom-repeat` as part of template parsing.
*
* @param {boolean} useRemoveNestedTemplates enable or disable removing nested
* templates during parsing
* @return {void}
*/
export const setRemoveNestedTemplates = function(useRemoveNestedTemplates) {
removeNestedTemplates = useRemoveNestedTemplates;
};
/**
* Setting to place `dom-if` elements in a performance-optimized mode that takes
* advantage of lighter-weight host runtime template stamping to eliminate the
* need for an intermediate Templatizer `TemplateInstance` to mange the nodes
* stamped by `dom-if`. Under this setting, any Templatizer-provided API's
* such as `modelForElement` will not be available for nodes stamped by
* `dom-if`.
*/
export let fastDomIf = window.Polymer && window.Polymer.fastDomIf || false;
/**
* Sets `fastDomIf` globally, to put `dom-if` in a performance-optimized mode.
*
* @param {boolean} useFastDomIf enable or disable `dom-if` fast-mode
* @return {void}
*/
export const setFastDomIf = function(useFastDomIf) {
fastDomIf = useFastDomIf;
};
+106 -68
View File
@@ -102,10 +102,49 @@ function upgradeTemplate(template, constructor) {
* @implements {Polymer_PropertyEffects}
* @private
*/
const templateInstanceBase = PropertyEffects(
// This cast shouldn't be neccessary, but Closure doesn't understand that
// "class {}" is a constructor function.
/** @type {function(new:Object)} */(class {}));
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
@@ -212,45 +251,7 @@ class TemplateInstanceBase extends templateInstanceBase {
* @protected
*/
_showHideChildren(hide) {
let c = this.children;
for (let i=0; i<c.length; i++) {
let n = c[i];
// Ignore non-changes
if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) {
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);
}
}
}
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);
}
}
showHideChildren(hide, this.children);
}
/**
* Overrides default property-effects implementation to intercept
@@ -325,9 +326,9 @@ TemplateInstanceBase.prototype.__hostProps;
* @private
*/
const MutableTemplateInstanceBase = MutableData(
// This cast shouldn't be necessary, but Closure doesn't seem to understand
// this constructor.
/** @type {function(new:TemplateInstanceBase)} */(TemplateInstanceBase));
// 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.
@@ -372,23 +373,41 @@ function createTemplatizerClass(template, templateInfo, options) {
/**
* 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(template, templateInfo, options, methodHost) {
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) {
/**
* @constructor
* @extends {DataTemplate}
*/
let templatizedBase = options.mutableData ? MutableDataTemplate : DataTemplate;
/** @private */
klass = templateInfo.templatizeTemplateClass =
class TemplatizedTemplate extends templatizedBase {};
if (isTemplate) {
/**
* @constructor
* @extends {DataTemplate}
*/
let templatizedBase = options.mutableData ? MutableDataTemplate : DataTemplate;
/** @private */
klass = templateInfo.templatizeTemplateClass =
class TemplatizedTemplate extends templatizedBase {};
} 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
/** @private */
klass = templateInfo.templatizeTemplateClass =
class TemplatizedTemplateExtension extends templatizedBase {};
}
// Add template - >instances effects
// and host <- template effects
let hostProps = templateInfo.hostProps;
@@ -402,19 +421,36 @@ function addPropagateEffects(template, templateInfo, options, methodHost) {
warnOnUndeclaredProperties(templateInfo, options, methodHost);
}
}
upgradeTemplate(template, klass);
// Mix any pre-bound data into __data; no need to flush this to
// instances since they pull from the template at instance-time
if (template.__dataProto) {
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(template.__data, template.__dataProto);
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;
}
}
}
// Clear any pending data for performance
template.__dataTemp = {};
template.__dataPending = null;
template.__dataOld = null;
template._enableProperties();
}
}
/* eslint-enable valid-jsdoc */
@@ -604,7 +640,7 @@ function warnOnUndeclaredProperties(templateInfo, options, methodHost) {
}
}
}
}
}
}
/**
@@ -621,8 +657,10 @@ function warnOnUndeclaredProperties(templateInfo, options, methodHost) {
* model.set('item.checked', true);
* }
*
* @param {HTMLTemplateElement} template The model will be returned for
* elements stamped from this template
* @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
@@ -633,7 +671,7 @@ export function modelForElement(template, 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.__templatizeInstance)) {
if ((model = node.__dataHost ? node : node.__templatizeInstance)) {
// Found an element stamped by another template; keep walking up
// from its __dataHost
if (model.__dataHost != template) {
+4
View File
@@ -71,7 +71,11 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
'unit/path.html',
'unit/templatize.html',
'unit/dom-repeat.html',
'unit/dom-repeat.html?removeNestedTemplates=true',
'unit/dom-if.html',
'unit/dom-if.html?removeNestedTemplates=true',
'unit/dom-if.html?fastDomIf=true',
'unit/dom-if.html?removeNestedTemplates=true&fastDomIf=true',
'unit/dom-bind.html',
'unit/array-selector.html',
'unit/polymer-dom.html',
+17 -1
View File
@@ -301,9 +301,25 @@ Polymer({
return val;
}
});
Polymer({
is: 'prop-observer',
properties: {
prop: {
observer: 'propChanged'
}
},
created() {
this.propChanged = sinon.spy();
}
});
Polymer({
_template: html`
<template is="dom-if" if="{{b}}" restamp="{{restamp}}">{{guarded(a)}}</template>
<template is="dom-if" if="{{b}}" restamp="{{restamp}}">
{{guarded(a)}}
<prop-observer id="observer" prop="[[c.d]]"></prop-observer>
</template>
`,
is: 'x-guard-separate-props',
+14 -1
View File
@@ -14,6 +14,11 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script src="wct-browser-config.js"></script>
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
<script type="module">
import { setRemoveNestedTemplates, setFastDomIf } from '../../lib/utils/settings.js';
setRemoveNestedTemplates(location.search.match(/removeNestedTemplates/));
setFastDomIf(location.search.match(/fastDomIf/));
</script>
<script type="module" src="../../polymer-legacy.js"></script>
<script type="module" src="./dom-if-elements.js"></script>
</head>
@@ -867,18 +872,26 @@ suite('timing', function() {
let el = document.createElement('x-guard-separate-props');
el.restamp = restamp;
document.body.appendChild(el);
el.a = 'ok';
el.b = true;
flush();
el.a = 'ok';
el.c = {d: 'ok'};
assert.equal(el.shadowRoot.textContent.trim(), 'ok');
assert.equal(el.shadowRoot.querySelector('#observer').propChanged.callCount, 1);
el.b = false;
el.a = 'notok';
el.set('c.d', 'notok');
flush();
assert.equal(el.shadowRoot.textContent.trim(), '');
if (!restamp) {
assert.equal(el.shadowRoot.querySelector('#observer').propChanged.callCount, 1);
}
el.set('c.d', 'changed');
el.a = 'changed';
el.b = true;
flush();
assert.equal(el.shadowRoot.textContent.trim(), 'changed');
assert.equal(el.shadowRoot.querySelector('#observer').propChanged.callCount, restamp ? 1 : 2);
document.body.removeChild(el);
});
+2 -2
View File
@@ -464,9 +464,9 @@ Polymer({
Polymer({
_template: html`
<template is="dom-repeat" items="{{items}}" id="outer">
<template is="dom-if" if="">
<template is="dom-if" if="" name="outerIf">
<template is="dom-repeat" items="{{item.items}}" id="inner">
<template is="dom-if" if="">
<template is="dom-if" if="" name="innerIf">
<button on-click="handleClick">{{item.prop}}</button>
</template>
</template>
+4 -1
View File
@@ -14,6 +14,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script src="wct-browser-config.js"></script>
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
<script type="module">
import { setRemoveNestedTemplates } from '../../lib/utils/settings.js';
setRemoveNestedTemplates(location.search.match(/removeNestedTemplates/));
</script>
<script type="module" src="../../polymer-legacy.js"></script>
<script type="module" src="./dom-repeat-elements.js"></script>
<style>
@@ -108,7 +112,6 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
</dom-repeat>
</div>
<test-fixture id="primitiveLarge">
<template>
<x-primitive-large></x-primitive-large>
+2 -2
View File
@@ -198,8 +198,8 @@ class XParsing extends PolymerElement {
}
return noted;
}
_bindTemplate(template) {
return this.templateInfoForTesting = super._bindTemplate(template);
_bindTemplate(template, instanceBinding) {
return this.templateInfoForTesting = super._bindTemplate(template, instanceBinding);
}
}
customElements.define('x-parsing', XParsing);