From ea4e7d970869881248d1b79b86baf19598838e2f Mon Sep 17 00:00:00 2001 From: Kevin Schaaf Date: Thu, 6 Apr 2017 02:33:03 -0700 Subject: [PATCH] Improvements to binding API: - Adds override points for _parseBindings and _evaluateBinding - Adds support for runtime template binding - Moves ready(), _hasAccessor tracking, and instance property swizzle at ready time to PropertyAccessors --- lib/elements/dom-bind.html | 3 +- lib/mixins/element-mixin.html | 9 +- lib/mixins/property-accessors.html | 86 ++- lib/mixins/property-effects.html | 863 +++++++++++++++-------------- lib/mixins/template-stamp.html | 35 +- lib/utils/templatize.html | 2 +- test/unit/templatize.html | 4 +- 7 files changed, 540 insertions(+), 462 deletions(-) diff --git a/lib/elements/dom-bind.html b/lib/elements/dom-bind.html index d08ca0e37c..b91438c364 100644 --- a/lib/elements/dom-bind.html +++ b/lib/elements/dom-bind.html @@ -85,8 +85,7 @@ observer.observe(this, {childList: true}); return; } - this._bindTemplate(template); - this.root = this._stampTemplate(template); + this.root = this._stampBoundTemplate(template); this.__children = []; for (let n=this.root.firstChild; n; n=n.nextSibling) { this.__children[this.__children.length] = n; diff --git a/lib/mixins/element-mixin.html b/lib/mixins/element-mixin.html index 30af1ec1a7..17e1287264 100644 --- a/lib/mixins/element-mixin.html +++ b/lib/mixins/element-mixin.html @@ -420,7 +420,7 @@ if (window.ShadyCSS) { window.ShadyCSS.prepareTemplate(template, is, ext); } - proto._bindTemplate(template, propertiesForClass(proto.constructor)); + proto._bindTemplate(template); } function flushPropertiesStub() {} @@ -620,7 +620,7 @@ ready() { if (this._template) { hostStack.beginHosting(this); - this.root = this._stampTemplate(this._template); + this.root = this._stampBoundTemplate(this._template); hostStack.endHosting(this); } super.ready(); @@ -745,6 +745,11 @@ return Polymer.ResolveUrl.resolveUrl(url, base); } + static _parseTemplateContent(template, templateInfo, nodeInfo) { + templateInfo.dynamicFns = templateInfo.dynamicFns || propertiesForClass(this); + return super._parseTemplateContent(template, templateInfo, nodeInfo); + } + } return PolymerElement; diff --git a/lib/mixins/property-accessors.html b/lib/mixins/property-accessors.html index 8d1c03074e..e045d2ef15 100644 --- a/lib/mixins/property-accessors.html +++ b/lib/mixins/property-accessors.html @@ -129,6 +129,7 @@ _initializeProperties() { this.__serializing = false; this.__dataCounter = 0; + this.__dataInitialized = false; this.__dataInvalid = false; // initialize data with prototype values saved when creating accessors this.__data = {}; @@ -137,6 +138,16 @@ if (this.__dataProto) { this._initializeProtoProperties(this.__dataProto); } + // Capture instance properties; these will be set into accessors + // during first flush. Don't set them here, since we want + // these to overwrite defaults/constructor assignments + for (let p in this.__dataHasAccessor) { + if (this.hasOwnProperty(p)) { + this.__dataInstanceProps = this.__dataInstanceProps || {}; + this.__dataInstanceProps[p] = this[p]; + delete this[p]; + } + } } /** @@ -347,15 +358,31 @@ * @protected */ _createPropertyAccessor(property, readOnly) { - saveAccessorValue(this, property); - Object.defineProperty(this, property, { - get: function() { - return this.__data[property]; - }, - set: readOnly ? function() { } : function(value) { - this._setProperty(property, value); - } - }); + if (!this.hasOwnProperty('__dataHasAccessor')) { + this.__dataHasAccessor = Object.assign({}, this.__dataHasAccessor); + } + if (!this.__dataHasAccessor[property]) { + this.__dataHasAccessor[property] = true; + saveAccessorValue(this, property); + Object.defineProperty(this, property, { + get: function() { + return this.__data[property]; + }, + set: readOnly ? function() { } : function(value) { + this._setProperty(property, value); + } + }); + } + } + + /** + * Returns true if this library created an accessor for the given property. + * + * @param {string} property Property name + * @return {boolean} True if an accessor was created + */ + _hasAccessor(property) { + return this.__dataHasAccessor && this.__dataHasAccessor[property]; } /** @@ -436,12 +463,41 @@ * @protected */ _flushProperties() { - let oldProps = this.__dataOld; - let changedProps = this.__dataPending; - this.__dataPending = null; - this.__dataCounter++; - this._propertiesChanged(this.__data, changedProps, oldProps); - this.__dataCounter--; + if (!this.__dataInitialized) { + this.ready() + } else if (this.__dataPending) { + let oldProps = this.__dataOld; + let changedProps = this.__dataPending; + this.__dataPending = null; + this.__dataCounter++; + this._propertiesChanged(this.__data, changedProps, oldProps); + this.__dataCounter--; + } + } + + /** + * Lifecycle callback called the first time properties are being flushed. + * Prior to `ready`, all property sets through accessors are queued and + * their effects are flushed after this method returns. + * + * Users may override this function to implement behavior that is + * dependent on the element having its properties initialized, e.g. + * from defaults (initialized from `constructor`, `_initializeProperties`), + * `attributeChangedCallback`, or values propagated from host e.g. via + * bindings. `super.ready()` must be called to ensure the data system + * becomes enabled. + * + * @public + */ + ready() { + // Update instance properties that shadowed proto accessors; these take + // priority over any defaults set in constructor or attributeChangedCallback + if (this.__dataInstanceProps) { + Object.assign(this, this.__dataInstanceProps); + } + this.__dataInitialized = true; + // Run normal flush + this._flushProperties(); } /** diff --git a/lib/mixins/property-effects.html b/lib/mixins/property-effects.html index 2de01ab393..0d4e023b31 100644 --- a/lib/mixins/property-effects.html +++ b/lib/mixins/property-effects.html @@ -32,7 +32,6 @@ // Property effect types; effects are stored on the prototype using these keys const TYPES = { - ANY: '__propertyEffects', COMPUTE: '__computeEffects', REFLECT: '__reflectEffects', NOTIFY: '__notifyEffects', @@ -90,14 +89,16 @@ * @param {string} type Type of effect to run * @param {Object} props Bag of current property changes * @param {Object=} oldProps Bag of previous values for changed properties + * @param {boolean} hasPaths True with `props` contains one or more paths + * @param {*} var_args Additional metadata to pass to effect function * @private */ - function runEffects(inst, effects, props, oldProps, hasPaths) { + function runEffects(inst, effects, props, oldProps, hasPaths, ...args) { if (effects) { let ran; let id = dedupeId++; for (let prop in props) { - if (runEffectsForProperty(inst, effects, id, prop, props, oldProps, hasPaths)) { + if (runEffectsForProperty(inst, effects, id, prop, props, oldProps, hasPaths, ...args)) { ran = true; } } @@ -110,13 +111,15 @@ * * @param {Object} inst The instance with effects to run * @param {Array} effects Array of effects - * @param {number} id Effect run id used for de-duping effects + * @param {number} dedupeId Counter used for de-duping effects * @param {string} prop Name of changed property - * @param {*} value Value of changed property - * @param {*} old Previous value of changed property + * @param {*} props Changed properties + * @param {*} oldProps Old properties + * @param {boolean} hasPaths True with `props` contains one or more paths + * @param {*} var_args Additional metadata to pass to effect function * @private */ - function runEffectsForProperty(inst, effects, dedupeId, prop, props, oldProps, hasPaths) { + function runEffectsForProperty(inst, effects, dedupeId, prop, props, oldProps, hasPaths, ...args) { let ran; let rootProperty = hasPaths ? Polymer.Path.root(prop) : prop; let fxs = effects[rootProperty]; @@ -124,10 +127,10 @@ for (let i=0, l=fxs.length, fx; (i-changed') * @param {Object} inst Host element instance handling the notification event - * @param {string} property Child element property that was bound - * @param {string} path Host property/path that was bound + * @param {string} fromProp Child element property that was bound + * @param {string} toPath Host property/path that was bound * @param {boolean} negate Whether the binding was negated * @private */ - function handleNotification(e, inst, property, path, negate) { + function handleNotification(event, inst, fromProp, toPath, negate) { let value; - let targetPath = e.detail && e.detail.path; - if (targetPath) { - path = Polymer.Path.translate(property, path, targetPath); - value = e.detail && e.detail.value; + let detail = event.detail; + let fromPath = detail && detail.path; + if (fromPath) { + toPath = Polymer.Path.translate(fromProp, toPath, fromPath); + value = detail && detail.value; } else { - value = e.target[property]; + value = event.target[fromProp]; } value = negate ? !value : value; - setPropertyFromNotification(inst, path, value, e); - } - - /** - * Called by 2-way binding notification event listeners to set a property - * or path to the host based on a notification from a bound child. - * - * @param {string} path Path on this instance to set - * @param {*} value Value to set to given path - * @protected - */ - function setPropertyFromNotification(inst, path, value, event) { - let detail = event.detail; - if (detail && detail.queueProperty) { - if (!inst.__readOnly || !inst.__readOnly[path]) { - inst._setPendingPropertyOrPath(path, value, true, Boolean(detail.path)); + if (!inst.__readOnly || !inst.__readOnly[toPath]) { + if (inst._setPendingPropertyOrPath(toPath, value, true, Boolean(fromPath)) + && (!detail || !detail.queueProperty)) { + inst._invalidateProperties(); } - } else { - inst.set(path, value); } } @@ -382,6 +374,7 @@ * @param {Element} inst The instance the effect will be run on * @param {Object} changedProps Bag of changed properties * @param {Object} oldProps Bag of previous values for changed properties + * @param {boolean} hasPaths True with `props` contains one or more paths * @private */ function runComputedEffects(inst, changedProps, oldProps, hasPaths) { @@ -412,7 +405,7 @@ function runComputedEffect(inst, property, props, oldProps, info) { let result = runMethodEffect(inst, property, props, oldProps, info); let computedProp = info.methodInfo; - if (inst.__propertyEffects && inst.__propertyEffects[computedProp]) { + if (inst.__dataHasAccessor && inst.__dataHasAccessor[computedProp]) { inst._setPendingProperty(computedProp, result, true); } else { inst[computedProp] = result; @@ -425,6 +418,7 @@ * * @param {Element} inst The instance whose props are changing * @param {Object} changedProps Bag of changed properties + * @param {boolean} hasPaths True with `props` contains one or more paths * @private */ function computeLinkedPaths(inst, changedProps, hasPaths) { @@ -453,6 +447,16 @@ // -- bindings ---------------------------------------------- + function addBinding(nodeInfo, kind, target, parts, literal) { + nodeInfo.bindings = nodeInfo.bindings || []; + let binding = { + kind, target, parts, literal, + isCompound: (parts.length !== 1) + }; + nodeInfo.bindings.push(binding); + return binding; + } + /** * Adds "binding" property effects for the template annotation * ("note" for short) and node index specified. These may either be normal @@ -462,37 +466,36 @@ * * @param {Object} model Prototype or instance * @param {Object} note Annotation note returned from Annotator - * @param {number} index Index into `__templateNodes` list of annotated nodes that the - * note applies to + * @param {number} index Index into `nodeList` that the binding applies to * @param {Object=} dynamicFns Map indicating whether method names should * be included as a dependency to the effect. * @private */ - function addBindingEffect(model, note, index, dynamicFns) { - for (let i=0; i info.value.length) && - (info.kind == 'property') && !info.isCompound && - node.__propertyEffects && node.__propertyEffects[info.name]) { + if (hasPaths && part.source && (path.length > part.source.length) && + (binding.kind == 'property') && !binding.isCompound && + node.__dataHasAccessor && node.__dataHasAccessor[binding.target]) { let value = props[path]; - path = Polymer.Path.translate(info.value, info.name, path); + path = Polymer.Path.translate(part.source, binding.target, path); if (node._setPendingPropertyOrPath(path, value, false, true)) { inst._enqueueClient(node); } } else { - // Root or deeper path was set; extract bound path value - // e.g.: foo="{{obj.sub}}", path: 'obj', set 'foo'=obj.sub - // or: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo'=obj.sub - if (path != info.value) { - value = Polymer.Path.get(inst, info.value); - } else { - if (hasPaths && Polymer.Path.isPath(path)) { - value = Polymer.Path.get(inst, path); - } else { - value = inst.__data[path]; - } - } + let value = info.evaluator._evaluateBinding(inst, part, path, props, oldProps, hasPaths); // Propagate value to child - applyBindingValue(inst, info, value); + applyBindingValue(inst, node, binding, part, value); } } @@ -548,19 +544,18 @@ * @param {*} value Value to set * @private */ - function applyBindingValue(inst, info, value) { - let node = inst.__templateNodes[info.index]; - value = computeBindingValue(node, value, info); + function applyBindingValue(inst, node, binding, part, value) { + value = computeBindingValue(node, value, binding, part); if (Polymer.sanitizeDOMValue) { - value = Polymer.sanitizeDOMValue(value, info.name, info.kind, node); + value = Polymer.sanitizeDOMValue(value, binding.target, binding.kind, node); } - if (info.kind == 'attribute') { + if (binding.kind == 'attribute') { // Attribute binding - inst._valueToNodeAttribute(node, value, info.name); + inst._valueToNodeAttribute(node, value, binding.target); } else { // Property binding - let prop = info.name; - if (node.__propertyEffects && node.__propertyEffects[prop]) { + let prop = binding.target; + if (node.__dataHasAccessor && node.__dataHasAccessor[prop]) { if (!node.__readOnly || !node.__readOnly[prop]) { if (node._setPendingProperty(prop, value)) { inst._enqueueClient(node); @@ -578,74 +573,26 @@ * * @param {Node} node Node the value will be set to * @param {*} value Value to set - * @param {Object} info Effect metadata + * @param {Object} binding Binding metadata * @return {*} Transformed value to set * @private */ - function computeBindingValue(node, value, info) { - if (info.negate) { - value = !value; - } - if (info.isCompound) { - let storage = node.__dataCompoundStorage[info.name]; - storage[info.compoundIndex] = value; + function computeBindingValue(node, value, binding, part) { + if (binding.isCompound) { + let storage = node.__dataCompoundStorage[binding.target]; + storage[part.compoundIndex] = value; value = storage.join(''); } - if (info.kind !== 'attribute') { + if (binding.kind !== 'attribute') { // Some browsers serialize `undefined` to `"undefined"` - if (info.name === 'textContent' || - (node.localName == 'input' && info.name == 'value')) { + if (binding.target === 'textContent' || + (node.localName == 'input' && binding.target == 'value')) { value = value == undefined ? '' : value; } } return value; } - /** - * Adds "binding method" property effects for the template binding - * ("note" for short), part metadata, and node index specified. - * - * @param {Object} model Prototype or instance - * @param {Object} note Binding note returned from Annotator - * @param {Object} part The compound part metadata - * @param {number} index Index into `__templateNodes` list of annotated nodes that the - * note applies to - * @param {Object=} dynamicFns Map indicating whether method names should - * be included as a dependency to the effect. - * @private - */ - function addMethodBindingEffect(model, note, part, index, dynamicFns) { - createMethodEffect(model, part.signature, TYPES.PROPAGATE, - runMethodBindingEffect, { - index: index, - isCompound: note.isCompound, - compoundIndex: part.compoundIndex, - kind: note.kind, - name: note.name, - negate: part.negate, - part: part - }, dynamicFns - ); - } - - /** - * Implements the "binding method" (inline computed function) effect. - * - * Runs the method with the values of the arguments specified in the `info` - * object and setting the return value to the node property/attribute. - * - * @param {Object} inst The instance the effect will be run on - * @param {string} property Name of property - * @param {*} value Current value of property - * @param {*} old Previous value of property - * @param {Object} info Effect metadata - * @private - */ - function runMethodBindingEffect(inst, property, props, oldProps, info) { - let val = runMethodEffect(inst, property, props, oldProps, info); - applyBindingValue(inst, info.methodInfo, val); - } - /** * Returns true if a binding's metadata meets all the requirements to allow * 2-way binding, and therefore a -changed event listener should be @@ -660,7 +607,7 @@ * @private */ function shouldAddListener(binding) { - return binding.name && + return binding.target && binding.kind != 'attribute' && binding.kind != 'text' && !binding.isCompound && @@ -672,60 +619,47 @@ * instance time to add event listeners for 2-way bindings. * * @param {Object} model Prototype (instances not currently supported) - * @param {number} index Index into `__templateNodes` list of annotated nodes that the - * event should be added to - * @param {string} property Property of target node to listen for changes - * @param {string} path Host path that the change should be propagated to - * @param {string=} event A custom event name to listen for (e.g. via the - * `{{prop::eventName}}` syntax) - * @param {boolean=} negate Whether the notified value should be negated before - * setting to host path - * @private - */ - function addAnnotatedListener(model, index, property, path, event, negate) { - event = event || (CaseMap.camelToDashCase(property) + '-changed'); - model.__notifyListeners = model.__notifyListeners || []; - model.__notifyListeners.push({ index, property, path, event, negate }); - } - - /** - * Adds all 2-way binding notification listeners to a host based on - * `__notifyListeners` metadata recorded by prior calls to`addAnnotatedListener` - * - * @param {Object} inst Host element instance + * @param {number} index Index into `nodeList` that the event should be added to + * @param {Object} binding Binding metadata * @private */ - function setupNotifyListeners(inst) { - let b$ = inst.__notifyListeners; - for (let i=0, l=b$.length, info; (i lastIndex) { - parts.push({literal: text.slice(lastIndex, m.index)}); - } - // Add binding part - // Mode (one-way or two) - let mode = m[1][0]; - let negate = Boolean(m[2]); - let value = m[3].trim(); - let customEvent, event, colon; - if (mode == '{' && (colon = value.indexOf('::')) > 0) { - event = value.substring(colon + 2); - value = value.substring(0, colon); - customEvent = true; - } - let signature = parseMethod(value, hostProps); - let rootProperty; - if (!signature) { - rootProperty = Polymer.Path.root(value); - hostProps[rootProperty] = true; - } - parts.push({ - value, mode, negate, event, customEvent, signature, rootProperty, - compoundIndex: parts.length - }); - lastIndex = bindingRegex.lastIndex; - } - // Add a final literal part - if (lastIndex && lastIndex < text.length) { - let literal = text.substring(lastIndex); - if (literal) { - parts.push({ literal }); - } - } - if (parts.length) { - return parts; - } - } - function literalFromParts(parts) { let s = ''; for (let i=0; i= 0) { + effects.splice(idx, 1); + } + } + /** * Returns whether the current prototype/instance has a property effect * of a certain type. @@ -1296,7 +1173,7 @@ * @protected */ _hasPropertyEffect(property, type) { - let effects = this[type || TYPES.ANY]; + let effects = this[type]; return Boolean(effects && effects[property]); } @@ -1378,9 +1255,9 @@ */ _setPendingPropertyOrPath(path, value, shouldNotify, isPathNotification) { let rootProperty = Polymer.Path.root(Array.isArray(path) ? path[0] : path); - let hasEffect = this.__propertyEffects && this.__propertyEffects[rootProperty]; + let hasAccessor = this.__dataHasAccessor && this.__dataHasAccessor[rootProperty]; let isPath = (rootProperty !== path); - if (hasEffect) { + if (hasAccessor) { if (isPath) { if (!isPathNotification) { // Dirty check changes being set to a path against the actual object, @@ -1590,55 +1467,14 @@ } /** - * Overrides PropertyAccessor's default async queuing of - * `_propertiesChanged`, to instead synchronously flush - * `_propertiesChanged` unless the `this._asyncEffects` property is true. - * - * If this is the first time properties are being flushed, the `ready` - * callback will be called. + * Overrides PropertyAccessor * * @override */ - _flushProperties() { - if (!this.__dataInitialized) { - this.ready() - } else if (this.__dataPending) { - super._flushProperties(); - if (!this.__dataCounter) { - // Clear temporary cache at end of turn - this.__dataTemp = {}; - } - } - } - - /** - * Polymer-specific lifecycle callback called the first time properties - * are being flushed. Prior to `ready`, all property sets through - * accessors are queued and their effects are flushed after this method - * returns. - * - * Users may override this function to implement behavior that is - * dependent on the element having its properties initialized, e.g. - * from defaults (initialized from `constructor`, `_initializeProperties`), - * `attributeChangedCallback`, or binding values propagated from host - * "binding effects". `super.ready()` must be called to ensure the - * data system becomes enabled. - * - * @public - */ ready() { - // Update instance properties that shadowed proto accessors; these take - // priority over any defaults set in `properties` or constructor - let instanceProps = this.__dataInstanceProps; - if (instanceProps) { - initalizeInstanceProperties(this, instanceProps); - } - // Enable acceessors - this.__dataInitialized = true; - if (this.__dataPending) { - // Run normal flush - this._flushProperties(); - } else { + let dataPending = this.__dataPending; + super.ready(); + if (!dataPending) { this._readyClients(); } } @@ -1654,30 +1490,6 @@ this.__dataClientsInitialized = true; } - /** - * Stamps the provided template and performs instance-time setup for - * Polymer template features, including data bindings, declarative event - * listeners, and the `this.$` map of `id`'s to nodes. A document fragment - * is returned containing the stamped DOM, ready for insertion into the - * DOM. - * - * Note that for host data to be bound into the stamped DOM, the template - * must have been previously bound to the prototype via a call to - * `_bindTemplate`, which performs one-time template binding work. - * - * Note that this method currently only supports being called once per - * instance. - * - * @param {HTMLTemplateElement} template Template to stamp - * @return {DocumentFragment} Cloned template content - * @protected - */ - _stampTemplate(template) { - let dom = super._stampTemplate(template); - setupBindings(this, template._templateInfo.nodeInfoList); - return dom; - } - /** * Implements `PropertyAccessors`'s properties changed callback. * @@ -1703,7 +1515,7 @@ let notifyProps = this.__dataToNotify; this.__dataToNotify = null; // Propagate properties to clients - runEffects(this, this.__propagateEffects, changedProps, oldProps, hasPaths); + this._propagatePropertyChanges(changedProps, oldProps, hasPaths); // Flush clients this._flushClients(); // Reflect properties @@ -1714,11 +1526,36 @@ if (notifyProps) { runNotifyEffects(this, notifyProps, changedProps, oldProps, hasPaths); } + // Clear temporary cache at end of turn + if (this.__dataCounter == 1) { + this.__dataTemp = {}; + } // ---------------------------- // window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c); // ---------------------------- } + /** + * Called to propagate any property changes to stamped template nodes + * managed by this element. + * + * @param {Object} changedProps Bag of changed properties + * @param {Object} oldProps Bag of previous values for changed properties + * @param {boolean} hasPaths True with `props` contains one or more paths + * @protected + */ + _propagatePropertyChanges(changedProps, oldProps, hasPaths) { + if (this.__propagateEffects) { + runEffects(this, this.__propagateEffects, changedProps, oldProps, hasPaths); + } + let templateInfo = this.__templateInfo; + while (templateInfo) { + runEffects(this, templateInfo.propertyEffects, changedProps, oldProps, + hasPaths, templateInfo.nodeList); + templateInfo = templateInfo.nextTemplateInfo; + } + } + /** * Aliases one data path as another, such that path notifications from one * are routed to the other. @@ -2141,37 +1978,104 @@ // -- binding ---------------------------------------------- /** - * Creates "binding" property effects for all binding bindings - * in the provided template that forward host properties into DOM stamped - * from the template via `_stampTemplate`. + * Parses the provided template to ensure binding effects are created + * for them, and then ensures property accessors are created for any + * dependent properties in the template. Binding effects for bound + * templates are stored in a linked list on the instance so that + * templates can be efficiently stamped and unstamped. + * + * This method may be called on the prototype (for prototypical template + * binding) once per prototype, or on the instance for either the + * prototypically bound template and/or one or more templates to stamp + * bound to the instance. * * @param {HTMLTemplateElement} template Template containing binding * bindings - * @param {Object=} dynamicFns Map indicating whether method names should - * be included as a dependency to the effect. + * @param {DocumentFragment=} dom Stamped DOM for this template to be + * bound to the instance (leave undefined for prototypical template + * binding) + * @return {Object} Template metadata object * @protected */ - _bindTemplate(template, dynamicFns) { - // Clear any existing propagation effects inherited from superClass - this.__propagateEffects = {}; - this.__notifyListeners = []; + _bindTemplate(template, dom) { let templateInfo = this.constructor._parseTemplate(template); - let nodeInfo = templateInfo.nodeInfoList; - for (let i=0, info; (i} Array of binding part metadata + * @protected + */ + static _parseBindings(text, templateInfo) { + let parts = []; + let lastIndex = 0; + let m; + // Example: "literal1{{prop}}literal2[[!compute(foo,bar)]]final" + // Regex matches: + // Iteration 1: Iteration 2: + // m[1]: '{{' '[[' + // m[2]: '' '!' + // m[3]: 'prop' 'compute(foo,bar)' + while ((m = bindingRegex.exec(text)) !== null) { + // Add literal part + if (m.index > lastIndex) { + parts.push({literal: text.slice(lastIndex, m.index)}); + } + // Add binding part + let mode = m[1][0]; + let negate = Boolean(m[2]); + let source = m[3].trim(); + let customEvent, notifyEvent, colon; + if (mode == '{' && (colon = source.indexOf('::')) > 0) { + notifyEvent = source.substring(colon + 2); + source = source.substring(0, colon); + customEvent = true; + } + let signature = parseMethod(source); + let dependencies = []; + if (signature) { + // Inline computed function + let {args, methodName} = signature; + for (let i=0; i.content * is removed and stored in notes as well. * - * Note that this method may only be called once per instance (it does - * not support stamping multiple templates per element instance). - * * @param {HTMLTemplateElement} template Template to stamp * @return {DocumentFragment} Cloned template content */ @@ -406,10 +407,10 @@ let dom = document.importNode(content, true); // NOTE: ShadyDom optimization indicating there is an insertion point dom.__noInsertionPoint = !templateInfo.hasInsertionPoint; + let nodes = dom.nodeList = new Array(nodeInfo.length); this.$ = {}; - this.__templateNodes = new Array(nodeInfo.length); - for (let i=0, l=nodeInfo.length, info, node; (i