Chromium Code Reviews| Index: sdk/lib/html/dart2js/html_dart2js.dart |
| diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart |
| index 0642db8346675bb0e63a868d6c9598f86b47abc5..d110043527aa265f8969ab7e28e17292eac5ab1b 100644 |
| --- a/sdk/lib/html/dart2js/html_dart2js.dart |
| +++ b/sdk/lib/html/dart2js/html_dart2js.dart |
| @@ -9,7 +9,6 @@ import 'dart:indexed_db'; |
| import 'dart:isolate'; |
| import 'dart:json' as json; |
| import 'dart:math'; |
| -import 'dart:mdv_observe_impl'; |
| import 'dart:typed_data'; |
| import 'dart:svg' as svg; |
| import 'dart:web_audio' as web_audio; |
| @@ -8268,21 +8267,6 @@ abstract class Element extends Node implements ElementTraversal native "Element" |
| throw new UnsupportedError("Not supported on this platform"); |
| } |
| - @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| - Map<String, StreamSubscription> _attributeBindings; |
| - |
| - // TODO(jmesserly): I'm concerned about adding these to every element. |
| - // Conceptually all of these belong on TemplateElement. They are here to |
| - // support browsers that don't have <template> yet. |
| - // However even in the polyfill they're restricted to certain tags |
| - // (see [isTemplate]). So we can probably convert it to a (public) mixin, and |
| - // only mix it in to the elements that need it. |
| - @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| - var _model; |
| - |
| - @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| - _TemplateIterator _templateIterator; |
| - |
| @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| Element _templateInstanceRef; |
| @@ -8292,125 +8276,6 @@ abstract class Element extends Node implements ElementTraversal native "Element" |
| bool _templateIsDecorated; |
| - // TODO(jmesserly): should path be optional, and default to empty path? |
| - // It is used that way in at least one path in JS TemplateElement tests |
| - // (see "BindImperative" test in original JS code). |
| - @Experimental |
| - void bind(String name, model, String path) { |
| - _bindElement(this, name, model, path); |
| - } |
| - |
| - // TODO(jmesserly): this is static to work around http://dartbug.com/10166 |
| - // Similar issue for unbind/unbindAll below. |
| - static void _bindElement(Element self, String name, model, String path) { |
| - if (self._bindTemplate(name, model, path)) return; |
| - |
| - if (self._attributeBindings == null) { |
| - self._attributeBindings = new Map<String, StreamSubscription>(); |
| - } |
| - |
| - self.xtag.attributes.remove(name); |
| - |
| - var changed; |
| - if (name.endsWith('?')) { |
| - name = name.substring(0, name.length - 1); |
| - |
| - changed = (value) { |
| - if (_Bindings._toBoolean(value)) { |
| - self.xtag.attributes[name] = ''; |
| - } else { |
| - self.xtag.attributes.remove(name); |
| - } |
| - }; |
| - } else { |
| - changed = (value) { |
| - // TODO(jmesserly): escape value if needed to protect against XSS. |
| - // See https://github.com/polymer-project/mdv/issues/58 |
| - self.xtag.attributes[name] = value == null ? '' : '$value'; |
| - }; |
| - } |
| - |
| - self.unbind(name); |
| - |
| - self._attributeBindings[name] = |
| - new PathObserver(model, path).bindSync(changed); |
| - } |
| - |
| - @Experimental |
| - void unbind(String name) { |
| - _unbindElement(this, name); |
| - } |
| - |
| - static _unbindElement(Element self, String name) { |
| - if (self._unbindTemplate(name)) return; |
| - if (self._attributeBindings != null) { |
| - var binding = self._attributeBindings.remove(name); |
| - if (binding != null) binding.cancel(); |
| - } |
| - } |
| - |
| - @Experimental |
| - void unbindAll() { |
| - _unbindAllElement(this); |
| - } |
| - |
| - static void _unbindAllElement(Element self) { |
| - self._unbindAllTemplate(); |
| - |
| - if (self._attributeBindings != null) { |
| - for (var binding in self._attributeBindings.values) { |
| - binding.cancel(); |
| - } |
| - self._attributeBindings = null; |
| - } |
| - } |
| - |
| - // TODO(jmesserly): unlike the JS polyfill, we can't mixin |
| - // HTMLTemplateElement at runtime into things that are semantically template |
| - // elements. So instead we implement it here with a runtime check. |
| - // If the bind succeeds, we return true, otherwise we return false and let |
| - // the normal Element.bind logic kick in. |
| - bool _bindTemplate(String name, model, String path) { |
| - if (isTemplate) { |
| - switch (name) { |
| - case 'bind': |
| - case 'repeat': |
| - case 'if': |
| - _ensureTemplate(); |
| - if (_templateIterator == null) { |
| - _templateIterator = new _TemplateIterator(this); |
| - } |
| - _templateIterator.inputs.bind(name, model, path); |
| - return true; |
| - } |
| - } |
| - return false; |
| - } |
| - |
| - bool _unbindTemplate(String name) { |
| - if (isTemplate) { |
| - switch (name) { |
| - case 'bind': |
| - case 'repeat': |
| - case 'if': |
| - _ensureTemplate(); |
| - if (_templateIterator != null) { |
| - _templateIterator.inputs.unbind(name); |
| - } |
| - return true; |
| - } |
| - } |
| - return false; |
| - } |
| - |
| - void _unbindAllTemplate() { |
| - if (isTemplate) { |
| - unbind('bind'); |
| - unbind('repeat'); |
| - unbind('if'); |
| - } |
| - } |
| - |
| /** |
| * Gets the template this node refers to. |
| * This is only supported if [isTemplate] is true. |
| @@ -8445,38 +8310,20 @@ abstract class Element extends Node implements ElementTraversal native "Element" |
| @Experimental |
| DocumentFragment createInstance() { |
| _ensureTemplate(); |
| - |
| - var template = ref; |
| - if (template == null) template = this; |
| - |
| - var instance = _Bindings._createDeepCloneAndDecorateTemplates( |
| - template.content, attributes['syntax']); |
| - |
| - if (TemplateElement._instanceCreated != null) { |
| - TemplateElement._instanceCreated.add(instance); |
| - } |
| - return instance; |
| + return mdvPackage(this).createInstance(); |
| } |
| /** |
| * The data model which is inherited through the tree. |
| * This is only supported if [isTemplate] is true. |
| - * |
| - * Setting this will destructive propagate the value to all descendant nodes, |
| - * and reinstantiate all of the nodes expanded by this template. |
| - * |
| - * Currently this does not support propagation through Shadow DOMs. |
| */ |
| @Experimental |
| - get model => _model; |
| + get model => mdvPackage(this).model; |
| @Experimental |
| void set model(value) { |
| _ensureTemplate(); |
| - |
| - var syntax = TemplateElement.syntax[attributes['syntax']]; |
| - _model = value; |
| - _Bindings._addBindings(this, model, syntax); |
| + mdvPackage(this).model = value; |
| } |
| // TODO(jmesserly): const set would be better |
| @@ -12512,61 +12359,6 @@ class InputElement extends _HTMLElement implements |
| return e; |
| } |
| - @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| - _ValueBinding _valueBinding; |
| - |
| - @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| - _CheckedBinding _checkedBinding; |
| - |
| - @Experimental |
| - void bind(String name, model, String path) { |
| - switch (name) { |
| - case 'value': |
| - unbind('value'); |
| - attributes.remove('value'); |
| - _valueBinding = new _ValueBinding(this, model, path); |
| - break; |
| - case 'checked': |
| - unbind('checked'); |
| - attributes.remove('checked'); |
| - _checkedBinding = new _CheckedBinding(this, model, path); |
| - break; |
| - default: |
| - // TODO(jmesserly): this should be "super" (http://dartbug.com/10166). |
| - // Similar issue for unbind/unbindAll below. |
| - Element._bindElement(this, name, model, path); |
| - break; |
| - } |
| - } |
| - |
| - @Experimental |
| - void unbind(String name) { |
| - switch (name) { |
| - case 'value': |
| - if (_valueBinding != null) { |
| - _valueBinding.unbind(); |
| - _valueBinding = null; |
| - } |
| - break; |
| - case 'checked': |
| - if (_checkedBinding != null) { |
| - _checkedBinding.unbind(); |
| - _checkedBinding = null; |
| - } |
| - break; |
| - default: |
| - Element._unbindElement(this, name); |
| - break; |
| - } |
| - } |
| - |
| - @Experimental |
| - void unbindAll() { |
| - unbind('value'); |
| - unbind('checked'); |
| - Element._unbindAllElement(this); |
| - } |
| - |
| @DomName('HTMLInputElement.webkitSpeechChangeEvent') |
| @DocsEditable |
| @@ -16477,6 +16269,44 @@ class _ChildNodeListLazy extends ListBase<Node> { |
| Node operator[](int index) => _this.$dom_childNodes[index]; |
| } |
| +/** |
| + * The MDV package, if available. |
| + * |
| + * This can be used to initialize MDV support via: |
| + * |
| + * import 'dart:html'; |
| + * import 'package:mdv/mdv.dart' as mdv; |
| + * main() { |
| + * mdv.initialize(); |
| + * } |
| + */ |
| +Function mdvPackage = (node) { |
|
blois
2013/06/26 18:39:37
I'm a bit concerned about adding unqualified top-l
Jennifer Messerly
2013/06/26 18:47:24
That's fair. I feel the same way. I desperately wi
Jennifer Messerly
2013/06/26 22:27:10
Chatting with Pete it sounds like the main concern
|
| + throw new UnsupportedError("The MDV package is not available. " |
| + "You can enable it with `import 'package:mdv/mdv.dart' as mdv;` and " |
| + "`mdv.initialize()`"); |
| +}; |
| + |
| +/** Information about the instantiated template. */ |
| +class TemplateInstance { |
| + // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
| + // in cases where script has modified the template instance boundary. |
| + |
| + /** The first node of this template instantiation. */ |
| + final Node firstNode; |
| + |
| + /** |
| + * The last node of this template instantiation. |
| + * This could be identical to [firstNode] if the template only expanded to a |
| + * single node. |
| + */ |
| + final Node lastNode; |
| + |
| + /** The model used to instantiate the template. */ |
| + final model; |
| + |
| + TemplateInstance(this.firstNode, this.lastNode, this.model); |
| +} |
| + |
| @DomName('Node') |
| class Node extends EventTarget native "Node" { |
| List<Node> get nodes { |
| @@ -16557,26 +16387,24 @@ class Node extends EventTarget native "Node" { |
| */ |
| @Experimental |
| void bind(String name, model, String path) { |
| - // TODO(jmesserly): should we throw instead? |
| - window.console.error('Unhandled binding to Node: ' |
| - '$this $name $model $path'); |
| + mdvPackage(this).bind(name, model, path); |
| } |
| /** Unbinds the attribute [name]. */ |
| @Experimental |
| - void unbind(String name) {} |
| + void unbind(String name) { |
| + mdvPackage(this).unbind(name); |
| + } |
| /** Unbinds all bound attributes. */ |
| @Experimental |
| - void unbindAll() {} |
| - |
| - TemplateInstance _templateInstance; |
| + void unbindAll() { |
| + mdvPackage(this).unbindAll(); |
| + } |
| /** Gets the template instance that instantiated this node, if any. */ |
| @Experimental |
| - TemplateInstance get templateInstance => |
| - _templateInstance != null ? _templateInstance : |
| - (parent != null ? parent.templateInstance : null); |
| + TemplateInstance get templateInstance => mdvPackage(this).templateInstance; |
| @DomName('Node.ATTRIBUTE_NODE') |
| @@ -21016,6 +20844,99 @@ class TableSectionElement extends _HTMLElement native "HTMLTableSectionElement" |
| // WARNING: Do not edit - generated code. |
| + |
| +/** |
| + * Model-Driven Views (MDV)'s native features enables a wide-range of use cases, |
| + * but (by design) don't attempt to implement a wide array of specialized |
| + * behaviors. |
| + * |
| + * Enabling these features in MDV is a matter of implementing and registering an |
| + * MDV Custom Syntax. A Custom Syntax is an object which contains one or more |
| + * delegation functions which implement specialized behavior. This object is |
| + * registered with MDV via [TemplateElement.syntax]: |
| + * |
| + * |
| + * HTML: |
| + * <template bind syntax="MySyntax"> |
| + * {{ What!Ever('crazy')->thing^^^I+Want(data) }} |
| + * </template> |
| + * |
| + * Dart: |
| + * class MySyntax extends CustomBindingSyntax { |
| + * getBinding(model, path, name, node) { |
| + * // The magic happens here! |
| + * } |
| + * } |
| + * |
| + * ... |
| + * |
| + * TemplateElement.syntax['MySyntax'] = new MySyntax(); |
| + * |
| + * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for |
| + * more information about Custom Syntax. |
| + */ |
| +// TODO(jmesserly): if this is just one method, a function type would make it |
| +// more Dart-friendly. |
| +@Experimental |
| +abstract class CustomBindingSyntax { |
| + /** |
| + * This syntax method allows for a custom interpretation of the contents of |
| + * mustaches (`{{` ... `}}`). |
| + * |
| + * When a template is inserting an instance, it will invoke this method for |
| + * each mustache which is encountered. The function is invoked with four |
| + * arguments: |
| + * |
| + * - [model]: The data context for which this instance is being created. |
| + * - [path]: The text contents (trimmed of outer whitespace) of the mustache. |
| + * - [name]: The context in which the mustache occurs. Within element |
| + * attributes, this will be the name of the attribute. Within text, |
| + * this will be 'text'. |
| + * - [node]: A reference to the node to which this binding will be created. |
| + * |
| + * If the method wishes to handle binding, it is required to return an object |
| + * which has at least a `value` property that can be observed. If it does, |
| + * then MDV will call [Node.bind on the node: |
| + * |
| + * node.bind(name, retval, 'value'); |
| + * |
| + * If the 'getBinding' does not wish to override the binding, it should return |
| + * null. |
| + */ |
| + // TODO(jmesserly): I had to remove type annotations from "name" and "node" |
| + // Normally they are String and Node respectively. But sometimes it will pass |
| + // (int name, CompoundBinding node). That seems very confusing; we may want |
| + // to change this API. |
| + getBinding(model, String path, name, node) => null; |
| + |
| + /** |
| + * This syntax method allows a syntax to provide an alterate model than the |
| + * one the template would otherwise use when producing an instance. |
| + * |
| + * When a template is about to create an instance, it will invoke this method |
| + * The function is invoked with two arguments: |
| + * |
| + * - [template]: The template element which is about to create and insert an |
| + * instance. |
| + * - [model]: The data context for which this instance is being created. |
| + * |
| + * The template element will always use the return value of `getInstanceModel` |
| + * as the model for the new instance. If the syntax does not wish to override |
| + * the value, it should simply return the `model` value it was passed. |
| + */ |
| + getInstanceModel(Element template, model) => model; |
| + |
| + /** |
| + * This syntax method allows a syntax to provide an alterate expansion of |
| + * the [template] contents. When the template wants to create an instance, |
| + * it will call this method with the template element. |
| + * |
| + * By default this will call `template.createInstance()`. |
| + */ |
| + getInstanceFragment(Element template) => template.createInstance(); |
| +} |
| + |
| + |
| @Experimental |
| @DomName('HTMLTemplateElement') |
| @SupportedBrowser(SupportedBrowser.CHROME) |
| @@ -21041,25 +20962,6 @@ class TemplateElement extends _HTMLElement native "HTMLTemplateElement" { |
| @Experimental |
| DocumentFragment get content => $dom_content; |
| - static StreamController<DocumentFragment> _instanceCreated; |
| - |
| - /** |
| - * *Warning*: This is an implementation helper for Model-Driven Views and |
| - * should not be used in your code. |
| - * |
| - * This event is fired whenever a template is instantiated via |
| - * [createInstance]. |
| - */ |
| - // TODO(rafaelw): This is a hack, and is neccesary for the polyfill |
| - // because custom elements are not upgraded during clone() |
| - @Experimental |
| - static Stream<DocumentFragment> get instanceCreated { |
| - if (_instanceCreated == null) { |
| - _instanceCreated = new StreamController<DocumentFragment>(sync: true); |
| - } |
| - return _instanceCreated.stream; |
| - } |
| - |
| /** |
| * Ensures proper API and content model for template elements. |
| * |
| @@ -21081,7 +20983,7 @@ class TemplateElement extends _HTMLElement native "HTMLTemplateElement" { |
| // Create content |
| if (template is! TemplateElement) { |
| - var doc = _Bindings._getTemplateContentsOwner(template.document); |
| + var doc = _getTemplateContentsOwner(template.document); |
| template._templateContent = doc.createDocumentFragment(); |
| } |
| @@ -21093,43 +20995,112 @@ class TemplateElement extends _HTMLElement native "HTMLTemplateElement" { |
| if (template is TemplateElement) { |
| bootstrap(template.content); |
| } else { |
| - _Bindings._liftNonNativeChildrenIntoContent(template); |
| + _liftNonNativeChildrenIntoContent(template); |
| } |
| return true; |
| } |
| - /** |
| - * This used to decorate recursively all templates from a given node. |
| - * |
| - * By default [decorate] will be called on templates lazily when certain |
| - * properties such as [model] are accessed, but it can be run eagerly to |
| - * decorate an entire tree recursively. |
| - */ |
| - // TODO(rafaelw): Review whether this is the right public API. |
| - @Experimental |
| - static void bootstrap(Node content) { |
| - _Bindings._bootstrapTemplatesRecursivelyFrom(content); |
| - } |
| - |
| - /** |
| - * Binds all mustaches recursively starting from the [root] node. |
| - * |
| - * Note: this is not an official Model-Driven-Views API; it is intended to |
| - * support binding the [ShadowRoot]'s content to a model. |
| - */ |
| - // TODO(jmesserly): this is needed to avoid two <template> nodes when using |
| - // bindings in a custom element's template. See also: |
| - // https://github.com/polymer-project/polymer/blob/master/src/bindMDV.js#L68 |
| - // Called from: |
| - // https://github.com/polymer-project/polymer/blob/master/src/register.js#L99 |
| - @Experimental |
| - static void bindModel(Node root, model, [CustomBindingSyntax syntax]) { |
| - _Bindings._addBindings(root, model, syntax); |
| + // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner |
| + static Document _getTemplateContentsOwner(HtmlDocument doc) { |
| + if (doc.window == null) { |
| + return doc; |
| + } |
| + var d = doc._templateContentsOwner; |
| + if (d == null) { |
| + // TODO(arv): This should either be a Document or HTMLDocument depending |
| + // on doc. |
| + d = doc.implementation.createHtmlDocument(''); |
| + while (d.$dom_lastChild != null) { |
| + d.$dom_lastChild.remove(); |
| + } |
| + doc._templateContentsOwner = d; |
| + } |
| + return d; |
| } |
| - static bool _initStyles; |
| - |
| + static Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
| + var clone = templateElement.clone(false); |
| + var attributes = templateElement.attributes; |
| + for (var name in attributes.keys.toList()) { |
| + switch (name) { |
| + case 'template': |
| + case 'repeat': |
| + case 'bind': |
| + case 'ref': |
| + clone.attributes.remove(name); |
| + break; |
| + default: |
| + attributes.remove(name); |
| + break; |
| + } |
| + } |
| + |
| + return clone; |
| + } |
| + |
| + static void _liftNonNativeChildrenIntoContent(Element templateElement) { |
| + var content = templateElement.content; |
| + |
| + if (!templateElement._isAttributeTemplate) { |
| + var child; |
| + while ((child = templateElement.$dom_firstChild) != null) { |
| + content.append(child); |
| + } |
| + return; |
| + } |
| + |
| + // For attribute templates we copy the whole thing into the content and |
| + // we move the non template attributes into the content. |
| + // |
| + // <tr foo template> |
| + // |
| + // becomes |
| + // |
| + // <tr template> |
| + // + #document-fragment |
| + // + <tr foo> |
| + // |
| + var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
| + var child; |
| + while ((child = templateElement.$dom_firstChild) != null) { |
| + newRoot.append(child); |
| + } |
| + content.append(newRoot); |
| + } |
| + |
| + /** |
| + * This used to decorate recursively all templates from a given node. |
| + * |
| + * By default [decorate] will be called on templates lazily when certain |
| + * properties such as [model] are accessed, but it can be run eagerly to |
| + * decorate an entire tree recursively. |
| + */ |
| + // TODO(rafaelw): Review whether this is the right public API. |
| + @Experimental |
| + static void bootstrap(Node content) { |
| + void _bootstrap(template) { |
| + if (!TemplateElement.decorate(template)) { |
| + bootstrap(template.content); |
| + } |
| + } |
| + |
| + // Need to do this first as the contents may get lifted if |node| is |
| + // template. |
| + // TODO(jmesserly): content is DocumentFragment or Element |
| + var descendents = (content as dynamic).queryAll(_allTemplatesSelectors); |
| + if (content is Element && (content as Element).isTemplate) { |
| + _bootstrap(content); |
| + } |
| + |
| + descendents.forEach(_bootstrap); |
| + } |
| + |
| + static final String _allTemplatesSelectors = 'template, option[template], ' + |
| + Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
| + |
| + static bool _initStyles; |
| + |
| static void _injectStylesheet() { |
| if (_initStyles == true) return; |
| _initStyles = true; |
| @@ -21184,42 +21155,6 @@ class Text extends CharacterData native "Text" { |
| @DocsEditable |
| Text splitText(int offset) native; |
| - |
| - @Creates('Null') // Set from Dart code; does not instantiate a native type. |
| - StreamSubscription _textBinding; |
| - |
| - @Experimental |
| - void bind(String name, model, String path) { |
| - if (name != 'text') { |
| - super.bind(name, model, path); |
| - return; |
| - } |
| - |
| - unbind('text'); |
| - |
| - _textBinding = new PathObserver(model, path).bindSync((value) { |
| - text = value == null ? '' : '$value'; |
| - }); |
| - } |
| - |
| - @Experimental |
| - void unbind(String name) { |
| - if (name != 'text') { |
| - super.unbind(name); |
| - return; |
| - } |
| - |
| - if (_textBinding == null) return; |
| - |
| - _textBinding.cancel(); |
| - _textBinding = null; |
| - } |
| - |
| - @Experimental |
| - void unbindAll() { |
| - unbind('text'); |
| - super.unbindAll(); |
| - } |
| } |
| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| @@ -27685,794 +27620,6 @@ class Rect { |
| Point get bottomRight => new Point(this.left + this.width, |
| this.top + this.height); |
| } |
| -// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| -// for details. All rights reserved. Use of this source code is governed by a |
| -// BSD-style license that can be found in the LICENSE file. |
| - |
| - |
| -// This code is a port of Model-Driven-Views: |
| -// https://github.com/polymer-project/mdv |
| -// The code mostly comes from src/template_element.js |
| - |
| -typedef void _ChangeHandler(value); |
| - |
| -/** |
| - * Model-Driven Views (MDV)'s native features enables a wide-range of use cases, |
| - * but (by design) don't attempt to implement a wide array of specialized |
| - * behaviors. |
| - * |
| - * Enabling these features in MDV is a matter of implementing and registering an |
| - * MDV Custom Syntax. A Custom Syntax is an object which contains one or more |
| - * delegation functions which implement specialized behavior. This object is |
| - * registered with MDV via [TemplateElement.syntax]: |
| - * |
| - * |
| - * HTML: |
| - * <template bind syntax="MySyntax"> |
| - * {{ What!Ever('crazy')->thing^^^I+Want(data) }} |
| - * </template> |
| - * |
| - * Dart: |
| - * class MySyntax extends CustomBindingSyntax { |
| - * getBinding(model, path, name, node) { |
| - * // The magic happens here! |
| - * } |
| - * } |
| - * |
| - * ... |
| - * |
| - * TemplateElement.syntax['MySyntax'] = new MySyntax(); |
| - * |
| - * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for more |
| - * information about Custom Syntax. |
| - */ |
| -// TODO(jmesserly): if this is just one method, a function type would make it |
| -// more Dart-friendly. |
| -@Experimental |
| -abstract class CustomBindingSyntax { |
| - /** |
| - * This syntax method allows for a custom interpretation of the contents of |
| - * mustaches (`{{` ... `}}`). |
| - * |
| - * When a template is inserting an instance, it will invoke this method for |
| - * each mustache which is encountered. The function is invoked with four |
| - * arguments: |
| - * |
| - * - [model]: The data context for which this instance is being created. |
| - * - [path]: The text contents (trimmed of outer whitespace) of the mustache. |
| - * - [name]: The context in which the mustache occurs. Within element |
| - * attributes, this will be the name of the attribute. Within text, |
| - * this will be 'text'. |
| - * - [node]: A reference to the node to which this binding will be created. |
| - * |
| - * If the method wishes to handle binding, it is required to return an object |
| - * which has at least a `value` property that can be observed. If it does, |
| - * then MDV will call [Node.bind on the node: |
| - * |
| - * node.bind(name, retval, 'value'); |
| - * |
| - * If the 'getBinding' does not wish to override the binding, it should return |
| - * null. |
| - */ |
| - // TODO(jmesserly): I had to remove type annotations from "name" and "node" |
| - // Normally they are String and Node respectively. But sometimes it will pass |
| - // (int name, CompoundBinding node). That seems very confusing; we may want |
| - // to change this API. |
| - getBinding(model, String path, name, node) => null; |
| - |
| - /** |
| - * This syntax method allows a syntax to provide an alterate model than the |
| - * one the template would otherwise use when producing an instance. |
| - * |
| - * When a template is about to create an instance, it will invoke this method |
| - * The function is invoked with two arguments: |
| - * |
| - * - [template]: The template element which is about to create and insert an |
| - * instance. |
| - * - [model]: The data context for which this instance is being created. |
| - * |
| - * The template element will always use the return value of `getInstanceModel` |
| - * as the model for the new instance. If the syntax does not wish to override |
| - * the value, it should simply return the `model` value it was passed. |
| - */ |
| - getInstanceModel(Element template, model) => model; |
| - |
| - /** |
| - * This syntax method allows a syntax to provide an alterate expansion of |
| - * the [template] contents. When the template wants to create an instance, |
| - * it will call this method with the template element. |
| - * |
| - * By default this will call `template.createInstance()`. |
| - */ |
| - getInstanceFragment(Element template) => template.createInstance(); |
| -} |
| - |
| -/** The callback used in the [CompoundBinding.combinator] field. */ |
| -@Experimental |
| -typedef Object CompoundBindingCombinator(Map objects); |
| - |
| -/** Information about the instantiated template. */ |
| -@Experimental |
| -class TemplateInstance { |
| - // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
| - // in cases where script has modified the template instance boundary. |
| - |
| - /** The first node of this template instantiation. */ |
| - final Node firstNode; |
| - |
| - /** |
| - * The last node of this template instantiation. |
| - * This could be identical to [firstNode] if the template only expanded to a |
| - * single node. |
| - */ |
| - final Node lastNode; |
| - |
| - /** The model used to instantiate the template. */ |
| - final model; |
| - |
| - TemplateInstance(this.firstNode, this.lastNode, this.model); |
| -} |
| - |
| -/** |
| - * Model-Driven Views contains a helper object which is useful for the |
| - * implementation of a Custom Syntax. |
| - * |
| - * var binding = new CompoundBinding((values) { |
| - * var combinedValue; |
| - * // compute combinedValue based on the current values which are provided |
| - * return combinedValue; |
| - * }); |
| - * binding.bind('name1', obj1, path1); |
| - * binding.bind('name2', obj2, path2); |
| - * //... |
| - * binding.bind('nameN', objN, pathN); |
| - * |
| - * CompoundBinding is an object which knows how to listen to multiple path |
| - * values (registered via [bind]) and invoke its [combinator] when one or more |
| - * of the values have changed and set its [value] property to the return value |
| - * of the function. When any value has changed, all current values are provided |
| - * to the [combinator] in the single `values` argument. |
| - * |
| - * See [CustomBindingSyntax] for more information. |
| - */ |
| -// TODO(jmesserly): what is the public API surface here? I just guessed; |
| -// most of it seemed non-public. |
| -@Experimental |
| -class CompoundBinding extends ObservableBase { |
| - CompoundBindingCombinator _combinator; |
| - |
| - // TODO(jmesserly): ideally these would be String keys, but sometimes we |
| - // use integers. |
| - Map<dynamic, StreamSubscription> _bindings = new Map(); |
| - Map _values = new Map(); |
| - bool _scheduled = false; |
| - bool _disposed = false; |
| - Object _value; |
| - |
| - CompoundBinding([CompoundBindingCombinator combinator]) { |
| - // TODO(jmesserly): this is a tweak to the original code, it seemed to me |
| - // that passing the combinator to the constructor should be equivalent to |
| - // setting it via the property. |
| - // I also added a null check to the combinator setter. |
| - this.combinator = combinator; |
| - } |
| - |
| - CompoundBindingCombinator get combinator => _combinator; |
| - |
| - set combinator(CompoundBindingCombinator combinator) { |
| - _combinator = combinator; |
| - if (combinator != null) _scheduleResolve(); |
| - } |
| - |
| - static const _VALUE = const Symbol('value'); |
| - |
| - get value => _value; |
| - |
| - void set value(newValue) { |
| - _value = notifyPropertyChange(_VALUE, _value, newValue); |
| - } |
| - |
| - // TODO(jmesserly): remove these workarounds when dart2js supports mirrors! |
| - getValueWorkaround(key) { |
| - if (key == _VALUE) return value; |
| - return null; |
| - } |
| - setValueWorkaround(key, val) { |
| - if (key == _VALUE) value = val; |
| - } |
| - |
| - void bind(name, model, String path) { |
| - unbind(name); |
| - |
| - _bindings[name] = new PathObserver(model, path).bindSync((value) { |
| - _values[name] = value; |
| - _scheduleResolve(); |
| - }); |
| - } |
| - |
| - void unbind(name, {bool suppressResolve: false}) { |
| - var binding = _bindings.remove(name); |
| - if (binding == null) return; |
| - |
| - binding.cancel(); |
| - _values.remove(name); |
| - if (!suppressResolve) _scheduleResolve(); |
| - } |
| - |
| - // TODO(rafaelw): Is this the right processing model? |
| - // TODO(rafaelw): Consider having a seperate ChangeSummary for |
| - // CompoundBindings so to excess dirtyChecks. |
| - void _scheduleResolve() { |
| - if (_scheduled) return; |
| - _scheduled = true; |
| - queueChangeRecords(resolve); |
| - } |
| - |
| - void resolve() { |
| - if (_disposed) return; |
| - _scheduled = false; |
| - |
| - if (_combinator == null) { |
| - throw new StateError( |
| - 'CompoundBinding attempted to resolve without a combinator'); |
| - } |
| - |
| - value = _combinator(_values); |
| - } |
| - |
| - void dispose() { |
| - for (var binding in _bindings.values) { |
| - binding.cancel(); |
| - } |
| - _bindings.clear(); |
| - _values.clear(); |
| - |
| - _disposed = true; |
| - value = null; |
| - } |
| -} |
| - |
| -abstract class _InputBinding { |
| - final InputElement element; |
| - PathObserver binding; |
| - StreamSubscription _pathSub; |
| - StreamSubscription _eventSub; |
| - |
| - _InputBinding(this.element, model, String path) { |
| - binding = new PathObserver(model, path); |
| - _pathSub = binding.bindSync(valueChanged); |
| - _eventSub = _getStreamForInputType(element).listen(updateBinding); |
| - } |
| - |
| - void valueChanged(newValue); |
| - |
| - void updateBinding(e); |
| - |
| - void unbind() { |
| - binding = null; |
| - _pathSub.cancel(); |
| - _eventSub.cancel(); |
| - } |
| - |
| - |
| - static Stream<Event> _getStreamForInputType(InputElement element) { |
| - switch (element.type) { |
| - case 'checkbox': |
| - return element.onClick; |
| - case 'radio': |
| - case 'select-multiple': |
| - case 'select-one': |
| - return element.onChange; |
| - default: |
| - return element.onInput; |
| - } |
| - } |
| -} |
| - |
| -class _ValueBinding extends _InputBinding { |
| - _ValueBinding(element, model, path) : super(element, model, path); |
| - |
| - void valueChanged(value) { |
| - element.value = value == null ? '' : '$value'; |
| - } |
| - |
| - void updateBinding(e) { |
| - binding.value = element.value; |
| - } |
| -} |
| - |
| -class _CheckedBinding extends _InputBinding { |
| - _CheckedBinding(element, model, path) : super(element, model, path); |
| - |
| - void valueChanged(value) { |
| - element.checked = _Bindings._toBoolean(value); |
| - } |
| - |
| - void updateBinding(e) { |
| - binding.value = element.checked; |
| - |
| - // Only the radio button that is getting checked gets an event. We |
| - // therefore find all the associated radio buttons and update their |
| - // CheckedBinding manually. |
| - if (element is InputElement && element.type == 'radio') { |
| - for (var r in _getAssociatedRadioButtons(element)) { |
| - var checkedBinding = r._checkedBinding; |
| - if (checkedBinding != null) { |
| - // Set the value directly to avoid an infinite call stack. |
| - checkedBinding.binding.value = false; |
| - } |
| - } |
| - } |
| - } |
| - |
| - // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
| - // Returns an array containing all radio buttons other than |element| that |
| - // have the same |name|, either in the form that |element| belongs to or, |
| - // if no form, in the document tree to which |element| belongs. |
| - // |
| - // This implementation is based upon the HTML spec definition of a |
| - // "radio button group": |
| - // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group |
| - // |
| - static Iterable _getAssociatedRadioButtons(element) { |
| - if (!_isNodeInDocument(element)) return []; |
| - if (element.form != null) { |
| - return element.form.nodes.where((el) { |
| - return el != element && |
| - el is InputElement && |
| - el.type == 'radio' && |
| - el.name == element.name; |
| - }); |
| - } else { |
| - var radios = element.document.queryAll( |
| - 'input[type="radio"][name="${element.name}"]'); |
| - return radios.where((el) => el != element && el.form == null); |
| - } |
| - } |
| - |
| - // TODO(jmesserly): polyfill document.contains API instead of doing it here |
| - static bool _isNodeInDocument(Node node) { |
| - // On non-IE this works: |
| - // return node.document.contains(node); |
| - var document = node.document; |
| - if (node == document || node.parentNode == document) return true; |
| - return document.documentElement.contains(node); |
| - } |
| -} |
| - |
| -class _Bindings { |
| - // TODO(jmesserly): not sure what kind of boolean conversion rules to |
| - // apply for template data-binding. HTML attributes are true if they're |
| - // present. However Dart only treats "true" as true. Since this is HTML we'll |
| - // use something closer to the HTML rules: null (missing) and false are false, |
| - // everything else is true. See: https://github.com/polymer-project/mdv/issues/59 |
| - static bool _toBoolean(value) => null != value && false != value; |
| - |
| - static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { |
| - var clone = node.clone(false); // Shallow clone. |
| - if (clone is Element && clone.isTemplate) { |
| - TemplateElement.decorate(clone, node); |
| - if (syntax != null) { |
| - clone.attributes.putIfAbsent('syntax', () => syntax); |
| - } |
| - } |
| - |
| - for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| - clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); |
| - } |
| - return clone; |
| - } |
| - |
| - // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner |
| - static Document _getTemplateContentsOwner(HtmlDocument doc) { |
| - if (doc.window == null) { |
| - return doc; |
| - } |
| - var d = doc._templateContentsOwner; |
| - if (d == null) { |
| - // TODO(arv): This should either be a Document or HTMLDocument depending |
| - // on doc. |
| - d = doc.implementation.createHtmlDocument(''); |
| - while (d.$dom_lastChild != null) { |
| - d.$dom_lastChild.remove(); |
| - } |
| - doc._templateContentsOwner = d; |
| - } |
| - return d; |
| - } |
| - |
| - static Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
| - var clone = templateElement.clone(false); |
| - var attributes = templateElement.attributes; |
| - for (var name in attributes.keys.toList()) { |
| - switch (name) { |
| - case 'template': |
| - case 'repeat': |
| - case 'bind': |
| - case 'ref': |
| - clone.attributes.remove(name); |
| - break; |
| - default: |
| - attributes.remove(name); |
| - break; |
| - } |
| - } |
| - |
| - return clone; |
| - } |
| - |
| - static void _liftNonNativeChildrenIntoContent(Element templateElement) { |
| - var content = templateElement.content; |
| - |
| - if (!templateElement._isAttributeTemplate) { |
| - var child; |
| - while ((child = templateElement.$dom_firstChild) != null) { |
| - content.append(child); |
| - } |
| - return; |
| - } |
| - |
| - // For attribute templates we copy the whole thing into the content and |
| - // we move the non template attributes into the content. |
| - // |
| - // <tr foo template> |
| - // |
| - // becomes |
| - // |
| - // <tr template> |
| - // + #document-fragment |
| - // + <tr foo> |
| - // |
| - var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
| - var child; |
| - while ((child = templateElement.$dom_firstChild) != null) { |
| - newRoot.append(child); |
| - } |
| - content.append(newRoot); |
| - } |
| - |
| - static void _bootstrapTemplatesRecursivelyFrom(Node node) { |
| - void bootstrap(template) { |
| - if (!TemplateElement.decorate(template)) { |
| - _bootstrapTemplatesRecursivelyFrom(template.content); |
| - } |
| - } |
| - |
| - // Need to do this first as the contents may get lifted if |node| is |
| - // template. |
| - // TODO(jmesserly): node is DocumentFragment or Element |
| - var descendents = (node as dynamic).queryAll(_allTemplatesSelectors); |
| - if (node is Element && (node as Element).isTemplate) bootstrap(node); |
| - |
| - descendents.forEach(bootstrap); |
| - } |
| - |
| - static final String _allTemplatesSelectors = 'template, option[template], ' + |
| - Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
| - |
| - static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
| - if (node is Element) { |
| - _addAttributeBindings(node, model, syntax); |
| - } else if (node is Text) { |
| - _parseAndBind(node, 'text', node.text, model, syntax); |
| - } |
| - |
| - for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| - _addBindings(c, model, syntax); |
| - } |
| - } |
| - |
| - static void _addAttributeBindings(Element element, model, syntax) { |
| - element.attributes.forEach((name, value) { |
| - if (value == '' && (name == 'bind' || name == 'repeat')) { |
| - value = '{{}}'; |
| - } |
| - _parseAndBind(element, name, value, model, syntax); |
| - }); |
| - } |
| - |
| - static void _parseAndBind(Node node, String name, String text, model, |
| - CustomBindingSyntax syntax) { |
| - |
| - var tokens = _parseMustacheTokens(text); |
| - if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
| - return; |
| - } |
| - |
| - // If this is a custom element, give the .xtag a change to bind. |
| - node = _nodeOrCustom(node); |
| - |
| - if (tokens.length == 1 && tokens[0].isBinding) { |
| - _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
| - return; |
| - } |
| - |
| - var replacementBinding = new CompoundBinding(); |
| - for (var i = 0; i < tokens.length; i++) { |
| - var token = tokens[i]; |
| - if (token.isBinding) { |
| - _bindOrDelegate(replacementBinding, i, model, token.value, syntax); |
| - } |
| - } |
| - |
| - replacementBinding.combinator = (values) { |
| - var newValue = new StringBuffer(); |
| - |
| - for (var i = 0; i < tokens.length; i++) { |
| - var token = tokens[i]; |
| - if (token.isText) { |
| - newValue.write(token.value); |
| - } else { |
| - var value = values[i]; |
| - if (value != null) { |
| - newValue.write(value); |
| - } |
| - } |
| - } |
| - |
| - return newValue.toString(); |
| - }; |
| - |
| - node.bind(name, replacementBinding, 'value'); |
| - } |
| - |
| - static void _bindOrDelegate(node, name, model, String path, |
| - CustomBindingSyntax syntax) { |
| - |
| - if (syntax != null) { |
| - var delegateBinding = syntax.getBinding(model, path, name, node); |
| - if (delegateBinding != null) { |
| - model = delegateBinding; |
| - path = 'value'; |
| - } |
| - } |
| - |
| - node.bind(name, model, path); |
| - } |
| - |
| - /** |
| - * Gets the [node]'s custom [Element.xtag] if present, otherwise returns |
| - * the node. This is used so nodes can override [Node.bind], [Node.unbind], |
| - * and [Node.unbindAll] like InputElement does. |
| - */ |
| - // TODO(jmesserly): remove this when we can extend Element for real. |
| - static _nodeOrCustom(node) => node is Element ? node.xtag : node; |
| - |
| - static List<_BindingToken> _parseMustacheTokens(String s) { |
| - var result = []; |
| - var length = s.length; |
| - var index = 0, lastIndex = 0; |
| - while (lastIndex < length) { |
| - index = s.indexOf('{{', lastIndex); |
| - if (index < 0) { |
| - result.add(new _BindingToken(s.substring(lastIndex))); |
| - break; |
| - } else { |
| - // There is a non-empty text run before the next path token. |
| - if (index > 0 && lastIndex < index) { |
| - result.add(new _BindingToken(s.substring(lastIndex, index))); |
| - } |
| - lastIndex = index + 2; |
| - index = s.indexOf('}}', lastIndex); |
| - if (index < 0) { |
| - var text = s.substring(lastIndex - 2); |
| - if (result.length > 0 && result.last.isText) { |
| - result.last.value += text; |
| - } else { |
| - result.add(new _BindingToken(text)); |
| - } |
| - break; |
| - } |
| - |
| - var value = s.substring(lastIndex, index).trim(); |
| - result.add(new _BindingToken(value, isBinding: true)); |
| - lastIndex = index + 2; |
| - } |
| - } |
| - return result; |
| - } |
| - |
| - static void _addTemplateInstanceRecord(fragment, model) { |
| - if (fragment.$dom_firstChild == null) { |
| - return; |
| - } |
| - |
| - var instanceRecord = new TemplateInstance( |
| - fragment.$dom_firstChild, fragment.$dom_lastChild, model); |
| - |
| - var node = instanceRecord.firstNode; |
| - while (node != null) { |
| - node._templateInstance = instanceRecord; |
| - node = node.nextNode; |
| - } |
| - } |
| - |
| - static void _removeAllBindingsRecursively(Node node) { |
| - _nodeOrCustom(node).unbindAll(); |
| - for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| - _removeAllBindingsRecursively(c); |
| - } |
| - } |
| - |
| - static void _removeChild(Node parent, Node child) { |
| - child._templateInstance = null; |
| - if (child is Element && (child as Element).isTemplate) { |
| - Element childElement = child; |
| - // Make sure we stop observing when we remove an element. |
| - var templateIterator = childElement._templateIterator; |
| - if (templateIterator != null) { |
| - templateIterator.abandon(); |
| - childElement._templateIterator = null; |
| - } |
| - } |
| - child.remove(); |
| - _removeAllBindingsRecursively(child); |
| - } |
| -} |
| - |
| -class _BindingToken { |
| - final String value; |
| - final bool isBinding; |
| - |
| - _BindingToken(this.value, {this.isBinding: false}); |
| - |
| - bool get isText => !isBinding; |
| -} |
| - |
| -class _TemplateIterator { |
| - final Element _templateElement; |
| - final List<Node> terminators = []; |
| - final CompoundBinding inputs; |
| - List iteratedValue; |
| - |
| - StreamSubscription _sub; |
| - StreamSubscription _valueBinding; |
| - |
| - _TemplateIterator(this._templateElement) |
| - : inputs = new CompoundBinding(resolveInputs) { |
| - |
| - _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
| - } |
| - |
| - static Object resolveInputs(Map values) { |
| - if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) { |
| - return null; |
| - } |
| - |
| - if (values.containsKey('repeat')) { |
| - return values['repeat']; |
| - } |
| - |
| - if (values.containsKey('bind')) { |
| - return [values['bind']]; |
| - } |
| - |
| - return null; |
| - } |
| - |
| - void valueChanged(value) { |
| - clear(); |
| - if (value is! List) return; |
| - |
| - iteratedValue = value; |
| - |
| - if (value is Observable) { |
| - _sub = value.changes.listen(_handleChanges); |
| - } |
| - |
| - int len = iteratedValue.length; |
| - if (len > 0) { |
| - _handleChanges([new ListChangeRecord(0, addedCount: len)]); |
| - } |
| - } |
| - |
| - Node getTerminatorAt(int index) { |
| - if (index == -1) return _templateElement; |
| - var terminator = terminators[index]; |
| - if (terminator is! Element) return terminator; |
| - |
| - var subIterator = terminator._templateIterator; |
| - if (subIterator == null) return terminator; |
| - |
| - return subIterator.getTerminatorAt(subIterator.terminators.length - 1); |
| - } |
| - |
| - void insertInstanceAt(int index, Node fragment) { |
| - var previousTerminator = getTerminatorAt(index - 1); |
| - var terminator = fragment.$dom_lastChild; |
| - if (terminator == null) terminator = previousTerminator; |
| - |
| - terminators.insert(index, terminator); |
| - var parent = _templateElement.parentNode; |
| - parent.insertBefore(fragment, previousTerminator.nextNode); |
| - } |
| - |
| - void removeInstanceAt(int index) { |
| - var previousTerminator = getTerminatorAt(index - 1); |
| - var terminator = getTerminatorAt(index); |
| - terminators.removeAt(index); |
| - |
| - var parent = _templateElement.parentNode; |
| - while (terminator != previousTerminator) { |
| - var node = terminator; |
| - terminator = node.previousNode; |
| - _Bindings._removeChild(parent, node); |
| - } |
| - } |
| - |
| - void removeAllInstances() { |
| - if (terminators.length == 0) return; |
| - |
| - var previousTerminator = _templateElement; |
| - var terminator = getTerminatorAt(terminators.length - 1); |
| - terminators.length = 0; |
| - |
| - var parent = _templateElement.parentNode; |
| - while (terminator != previousTerminator) { |
| - var node = terminator; |
| - terminator = node.previousNode; |
| - _Bindings._removeChild(parent, node); |
| - } |
| - } |
| - |
| - void clear() { |
| - unobserve(); |
| - removeAllInstances(); |
| - iteratedValue = null; |
| - } |
| - |
| - getInstanceModel(model, syntax) { |
| - if (syntax != null) { |
| - return syntax.getInstanceModel(_templateElement, model); |
| - } |
| - return model; |
| - } |
| - |
| - getInstanceFragment(syntax) { |
| - if (syntax != null) { |
| - return syntax.getInstanceFragment(_templateElement); |
| - } |
| - return _templateElement.createInstance(); |
| - } |
| - |
| - void _handleChanges(List<ListChangeRecord> splices) { |
| - var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; |
| - |
| - for (var splice in splices) { |
| - if (splice is! ListChangeRecord) continue; |
| - |
| - for (int i = 0; i < splice.removedCount; i++) { |
| - removeInstanceAt(splice.index); |
| - } |
| - |
| - for (var addIndex = splice.index; |
| - addIndex < splice.index + splice.addedCount; |
| - addIndex++) { |
| - |
| - var model = getInstanceModel(iteratedValue[addIndex], syntax); |
| - |
| - var fragment = getInstanceFragment(syntax); |
| - |
| - _Bindings._addBindings(fragment, model, syntax); |
| - _Bindings._addTemplateInstanceRecord(fragment, model); |
| - |
| - insertInstanceAt(addIndex, fragment); |
| - } |
| - } |
| - } |
| - |
| - void unobserve() { |
| - if (_sub == null) return; |
| - _sub.cancel(); |
| - _sub = null; |
| - } |
| - |
| - void abandon() { |
| - unobserve(); |
| - _valueBinding.cancel(); |
| - inputs.dispose(); |
| - } |
| -} |
| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |