Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(19)

Side by Side Diff: tools/dom/src/TemplateBindings.dart

Issue 17552019: Reorganize mdv and observe packages (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: merged Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/create_sdk.py ('k') | tools/dom/templates/html/dart2js/html_dart2js.darttemplate » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of html;
6
7 // This code is a port of Model-Driven-Views:
8 // https://github.com/polymer-project/mdv
9 // The code mostly comes from src/template_element.js
10
11 typedef void _ChangeHandler(value);
12
13 /**
14 * Model-Driven Views (MDV)'s native features enables a wide-range of use cases,
15 * but (by design) don't attempt to implement a wide array of specialized
16 * behaviors.
17 *
18 * Enabling these features in MDV is a matter of implementing and registering an
19 * MDV Custom Syntax. A Custom Syntax is an object which contains one or more
20 * delegation functions which implement specialized behavior. This object is
21 * registered with MDV via [TemplateElement.syntax]:
22 *
23 *
24 * HTML:
25 * <template bind syntax="MySyntax">
26 * {{ What!Ever('crazy')->thing^^^I+Want(data) }}
27 * </template>
28 *
29 * Dart:
30 * class MySyntax extends CustomBindingSyntax {
31 * getBinding(model, path, name, node) {
32 * // The magic happens here!
33 * }
34 * }
35 *
36 * ...
37 *
38 * TemplateElement.syntax['MySyntax'] = new MySyntax();
39 *
40 * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for m ore
41 * information about Custom Syntax.
42 */
43 // TODO(jmesserly): if this is just one method, a function type would make it
44 // more Dart-friendly.
45 @Experimental
46 abstract class CustomBindingSyntax {
47 /**
48 * This syntax method allows for a custom interpretation of the contents of
49 * mustaches (`{{` ... `}}`).
50 *
51 * When a template is inserting an instance, it will invoke this method for
52 * each mustache which is encountered. The function is invoked with four
53 * arguments:
54 *
55 * - [model]: The data context for which this instance is being created.
56 * - [path]: The text contents (trimmed of outer whitespace) of the mustache.
57 * - [name]: The context in which the mustache occurs. Within element
58 * attributes, this will be the name of the attribute. Within text,
59 * this will be 'text'.
60 * - [node]: A reference to the node to which this binding will be created.
61 *
62 * If the method wishes to handle binding, it is required to return an object
63 * which has at least a `value` property that can be observed. If it does,
64 * then MDV will call [Node.bind on the node:
65 *
66 * node.bind(name, retval, 'value');
67 *
68 * If the 'getBinding' does not wish to override the binding, it should return
69 * null.
70 */
71 // TODO(jmesserly): I had to remove type annotations from "name" and "node"
72 // Normally they are String and Node respectively. But sometimes it will pass
73 // (int name, CompoundBinding node). That seems very confusing; we may want
74 // to change this API.
75 getBinding(model, String path, name, node) => null;
76
77 /**
78 * This syntax method allows a syntax to provide an alterate model than the
79 * one the template would otherwise use when producing an instance.
80 *
81 * When a template is about to create an instance, it will invoke this method
82 * The function is invoked with two arguments:
83 *
84 * - [template]: The template element which is about to create and insert an
85 * instance.
86 * - [model]: The data context for which this instance is being created.
87 *
88 * The template element will always use the return value of `getInstanceModel`
89 * as the model for the new instance. If the syntax does not wish to override
90 * the value, it should simply return the `model` value it was passed.
91 */
92 getInstanceModel(Element template, model) => model;
93
94 /**
95 * This syntax method allows a syntax to provide an alterate expansion of
96 * the [template] contents. When the template wants to create an instance,
97 * it will call this method with the template element.
98 *
99 * By default this will call `template.createInstance()`.
100 */
101 getInstanceFragment(Element template) => template.createInstance();
102 }
103
104 /** The callback used in the [CompoundBinding.combinator] field. */
105 @Experimental
106 typedef Object CompoundBindingCombinator(Map objects);
107
108 /** Information about the instantiated template. */
109 @Experimental
110 class TemplateInstance {
111 // TODO(rafaelw): firstNode & lastNode should be read-synchronous
112 // in cases where script has modified the template instance boundary.
113
114 /** The first node of this template instantiation. */
115 final Node firstNode;
116
117 /**
118 * The last node of this template instantiation.
119 * This could be identical to [firstNode] if the template only expanded to a
120 * single node.
121 */
122 final Node lastNode;
123
124 /** The model used to instantiate the template. */
125 final model;
126
127 TemplateInstance(this.firstNode, this.lastNode, this.model);
128 }
129
130 /**
131 * Model-Driven Views contains a helper object which is useful for the
132 * implementation of a Custom Syntax.
133 *
134 * var binding = new CompoundBinding((values) {
135 * var combinedValue;
136 * // compute combinedValue based on the current values which are provided
137 * return combinedValue;
138 * });
139 * binding.bind('name1', obj1, path1);
140 * binding.bind('name2', obj2, path2);
141 * //...
142 * binding.bind('nameN', objN, pathN);
143 *
144 * CompoundBinding is an object which knows how to listen to multiple path
145 * values (registered via [bind]) and invoke its [combinator] when one or more
146 * of the values have changed and set its [value] property to the return value
147 * of the function. When any value has changed, all current values are provided
148 * to the [combinator] in the single `values` argument.
149 *
150 * See [CustomBindingSyntax] for more information.
151 */
152 // TODO(jmesserly): what is the public API surface here? I just guessed;
153 // most of it seemed non-public.
154 @Experimental
155 class CompoundBinding extends ObservableBase {
156 CompoundBindingCombinator _combinator;
157
158 // TODO(jmesserly): ideally these would be String keys, but sometimes we
159 // use integers.
160 Map<dynamic, StreamSubscription> _bindings = new Map();
161 Map _values = new Map();
162 bool _scheduled = false;
163 bool _disposed = false;
164 Object _value;
165
166 CompoundBinding([CompoundBindingCombinator combinator]) {
167 // TODO(jmesserly): this is a tweak to the original code, it seemed to me
168 // that passing the combinator to the constructor should be equivalent to
169 // setting it via the property.
170 // I also added a null check to the combinator setter.
171 this.combinator = combinator;
172 }
173
174 CompoundBindingCombinator get combinator => _combinator;
175
176 set combinator(CompoundBindingCombinator combinator) {
177 _combinator = combinator;
178 if (combinator != null) _scheduleResolve();
179 }
180
181 static const _VALUE = const Symbol('value');
182
183 get value => _value;
184
185 void set value(newValue) {
186 _value = notifyPropertyChange(_VALUE, _value, newValue);
187 }
188
189 // TODO(jmesserly): remove these workarounds when dart2js supports mirrors!
190 getValueWorkaround(key) {
191 if (key == _VALUE) return value;
192 return null;
193 }
194 setValueWorkaround(key, val) {
195 if (key == _VALUE) value = val;
196 }
197
198 void bind(name, model, String path) {
199 unbind(name);
200
201 _bindings[name] = new PathObserver(model, path).bindSync((value) {
202 _values[name] = value;
203 _scheduleResolve();
204 });
205 }
206
207 void unbind(name, {bool suppressResolve: false}) {
208 var binding = _bindings.remove(name);
209 if (binding == null) return;
210
211 binding.cancel();
212 _values.remove(name);
213 if (!suppressResolve) _scheduleResolve();
214 }
215
216 // TODO(rafaelw): Is this the right processing model?
217 // TODO(rafaelw): Consider having a seperate ChangeSummary for
218 // CompoundBindings so to excess dirtyChecks.
219 void _scheduleResolve() {
220 if (_scheduled) return;
221 _scheduled = true;
222 queueChangeRecords(resolve);
223 }
224
225 void resolve() {
226 if (_disposed) return;
227 _scheduled = false;
228
229 if (_combinator == null) {
230 throw new StateError(
231 'CompoundBinding attempted to resolve without a combinator');
232 }
233
234 value = _combinator(_values);
235 }
236
237 void dispose() {
238 for (var binding in _bindings.values) {
239 binding.cancel();
240 }
241 _bindings.clear();
242 _values.clear();
243
244 _disposed = true;
245 value = null;
246 }
247 }
248
249 abstract class _InputBinding {
250 final InputElement element;
251 PathObserver binding;
252 StreamSubscription _pathSub;
253 StreamSubscription _eventSub;
254
255 _InputBinding(this.element, model, String path) {
256 binding = new PathObserver(model, path);
257 _pathSub = binding.bindSync(valueChanged);
258 _eventSub = _getStreamForInputType(element).listen(updateBinding);
259 }
260
261 void valueChanged(newValue);
262
263 void updateBinding(e);
264
265 void unbind() {
266 binding = null;
267 _pathSub.cancel();
268 _eventSub.cancel();
269 }
270
271
272 static Stream<Event> _getStreamForInputType(InputElement element) {
273 switch (element.type) {
274 case 'checkbox':
275 return element.onClick;
276 case 'radio':
277 case 'select-multiple':
278 case 'select-one':
279 return element.onChange;
280 default:
281 return element.onInput;
282 }
283 }
284 }
285
286 class _ValueBinding extends _InputBinding {
287 _ValueBinding(element, model, path) : super(element, model, path);
288
289 void valueChanged(value) {
290 element.value = value == null ? '' : '$value';
291 }
292
293 void updateBinding(e) {
294 binding.value = element.value;
295 }
296 }
297
298 class _CheckedBinding extends _InputBinding {
299 _CheckedBinding(element, model, path) : super(element, model, path);
300
301 void valueChanged(value) {
302 element.checked = _Bindings._toBoolean(value);
303 }
304
305 void updateBinding(e) {
306 binding.value = element.checked;
307
308 // Only the radio button that is getting checked gets an event. We
309 // therefore find all the associated radio buttons and update their
310 // CheckedBinding manually.
311 if (element is InputElement && element.type == 'radio') {
312 for (var r in _getAssociatedRadioButtons(element)) {
313 var checkedBinding = r._checkedBinding;
314 if (checkedBinding != null) {
315 // Set the value directly to avoid an infinite call stack.
316 checkedBinding.binding.value = false;
317 }
318 }
319 }
320 }
321
322 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
323 // Returns an array containing all radio buttons other than |element| that
324 // have the same |name|, either in the form that |element| belongs to or,
325 // if no form, in the document tree to which |element| belongs.
326 //
327 // This implementation is based upon the HTML spec definition of a
328 // "radio button group":
329 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state. html#radio-button-group
330 //
331 static Iterable _getAssociatedRadioButtons(element) {
332 if (!_isNodeInDocument(element)) return [];
333 if (element.form != null) {
334 return element.form.nodes.where((el) {
335 return el != element &&
336 el is InputElement &&
337 el.type == 'radio' &&
338 el.name == element.name;
339 });
340 } else {
341 var radios = element.document.queryAll(
342 'input[type="radio"][name="${element.name}"]');
343 return radios.where((el) => el != element && el.form == null);
344 }
345 }
346
347 // TODO(jmesserly): polyfill document.contains API instead of doing it here
348 static bool _isNodeInDocument(Node node) {
349 // On non-IE this works:
350 // return node.document.contains(node);
351 var document = node.document;
352 if (node == document || node.parentNode == document) return true;
353 return document.documentElement.contains(node);
354 }
355 }
356
357 class _Bindings {
358 // TODO(jmesserly): not sure what kind of boolean conversion rules to
359 // apply for template data-binding. HTML attributes are true if they're
360 // present. However Dart only treats "true" as true. Since this is HTML we'll
361 // use something closer to the HTML rules: null (missing) and false are false,
362 // everything else is true. See: https://github.com/polymer-project/mdv/issues /59
363 static bool _toBoolean(value) => null != value && false != value;
364
365 static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) {
366 var clone = node.clone(false); // Shallow clone.
367 if (clone is Element && clone.isTemplate) {
368 TemplateElement.decorate(clone, node);
369 if (syntax != null) {
370 clone.attributes.putIfAbsent('syntax', () => syntax);
371 }
372 }
373
374 for (var c = node.firstChild; c != null; c = c.nextNode) {
375 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
376 }
377 return clone;
378 }
379
380 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html# dfn-template-contents-owner
381 static Document _getTemplateContentsOwner(HtmlDocument doc) {
382 if (doc.window == null) {
383 return doc;
384 }
385 var d = doc._templateContentsOwner;
386 if (d == null) {
387 // TODO(arv): This should either be a Document or HTMLDocument depending
388 // on doc.
389 d = doc.implementation.createHtmlDocument('');
390 while (d.lastChild != null) {
391 d.lastChild.remove();
392 }
393 doc._templateContentsOwner = d;
394 }
395 return d;
396 }
397
398 static Element _cloneAndSeperateAttributeTemplate(Element templateElement) {
399 var clone = templateElement.clone(false);
400 var attributes = templateElement.attributes;
401 for (var name in attributes.keys.toList()) {
402 switch (name) {
403 case 'template':
404 case 'repeat':
405 case 'bind':
406 case 'ref':
407 clone.attributes.remove(name);
408 break;
409 default:
410 attributes.remove(name);
411 break;
412 }
413 }
414
415 return clone;
416 }
417
418 static void _liftNonNativeChildrenIntoContent(Element templateElement) {
419 var content = templateElement.content;
420
421 if (!templateElement._isAttributeTemplate) {
422 var child;
423 while ((child = templateElement.firstChild) != null) {
424 content.append(child);
425 }
426 return;
427 }
428
429 // For attribute templates we copy the whole thing into the content and
430 // we move the non template attributes into the content.
431 //
432 // <tr foo template>
433 //
434 // becomes
435 //
436 // <tr template>
437 // + #document-fragment
438 // + <tr foo>
439 //
440 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement);
441 var child;
442 while ((child = templateElement.firstChild) != null) {
443 newRoot.append(child);
444 }
445 content.append(newRoot);
446 }
447
448 static void _bootstrapTemplatesRecursivelyFrom(Node node) {
449 void bootstrap(template) {
450 if (!TemplateElement.decorate(template)) {
451 _bootstrapTemplatesRecursivelyFrom(template.content);
452 }
453 }
454
455 // Need to do this first as the contents may get lifted if |node| is
456 // template.
457 // TODO(jmesserly): node is DocumentFragment or Element
458 var descendents = (node as dynamic).queryAll(_allTemplatesSelectors);
459 if (node is Element && (node as Element).isTemplate) bootstrap(node);
460
461 descendents.forEach(bootstrap);
462 }
463
464 static final String _allTemplatesSelectors = 'template, option[template], ' +
465 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
466
467 static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) {
468 if (node is Element) {
469 _addAttributeBindings(node, model, syntax);
470 } else if (node is Text) {
471 _parseAndBind(node, 'text', node.text, model, syntax);
472 }
473
474 for (var c = node.firstChild; c != null; c = c.nextNode) {
475 _addBindings(c, model, syntax);
476 }
477 }
478
479 static void _addAttributeBindings(Element element, model, syntax) {
480 element.attributes.forEach((name, value) {
481 if (value == '' && (name == 'bind' || name == 'repeat')) {
482 value = '{{}}';
483 }
484 _parseAndBind(element, name, value, model, syntax);
485 });
486 }
487
488 static void _parseAndBind(Node node, String name, String text, model,
489 CustomBindingSyntax syntax) {
490
491 var tokens = _parseMustacheTokens(text);
492 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
493 return;
494 }
495
496 // If this is a custom element, give the .xtag a change to bind.
497 node = _nodeOrCustom(node);
498
499 if (tokens.length == 1 && tokens[0].isBinding) {
500 _bindOrDelegate(node, name, model, tokens[0].value, syntax);
501 return;
502 }
503
504 var replacementBinding = new CompoundBinding();
505 for (var i = 0; i < tokens.length; i++) {
506 var token = tokens[i];
507 if (token.isBinding) {
508 _bindOrDelegate(replacementBinding, i, model, token.value, syntax);
509 }
510 }
511
512 replacementBinding.combinator = (values) {
513 var newValue = new StringBuffer();
514
515 for (var i = 0; i < tokens.length; i++) {
516 var token = tokens[i];
517 if (token.isText) {
518 newValue.write(token.value);
519 } else {
520 var value = values[i];
521 if (value != null) {
522 newValue.write(value);
523 }
524 }
525 }
526
527 return newValue.toString();
528 };
529
530 node.bind(name, replacementBinding, 'value');
531 }
532
533 static void _bindOrDelegate(node, name, model, String path,
534 CustomBindingSyntax syntax) {
535
536 if (syntax != null) {
537 var delegateBinding = syntax.getBinding(model, path, name, node);
538 if (delegateBinding != null) {
539 model = delegateBinding;
540 path = 'value';
541 }
542 }
543
544 node.bind(name, model, path);
545 }
546
547 /**
548 * Gets the [node]'s custom [Element.xtag] if present, otherwise returns
549 * the node. This is used so nodes can override [Node.bind], [Node.unbind],
550 * and [Node.unbindAll] like InputElement does.
551 */
552 // TODO(jmesserly): remove this when we can extend Element for real.
553 static _nodeOrCustom(node) => node is Element ? node.xtag : node;
554
555 static List<_BindingToken> _parseMustacheTokens(String s) {
556 var result = [];
557 var length = s.length;
558 var index = 0, lastIndex = 0;
559 while (lastIndex < length) {
560 index = s.indexOf('{{', lastIndex);
561 if (index < 0) {
562 result.add(new _BindingToken(s.substring(lastIndex)));
563 break;
564 } else {
565 // There is a non-empty text run before the next path token.
566 if (index > 0 && lastIndex < index) {
567 result.add(new _BindingToken(s.substring(lastIndex, index)));
568 }
569 lastIndex = index + 2;
570 index = s.indexOf('}}', lastIndex);
571 if (index < 0) {
572 var text = s.substring(lastIndex - 2);
573 if (result.length > 0 && result.last.isText) {
574 result.last.value += text;
575 } else {
576 result.add(new _BindingToken(text));
577 }
578 break;
579 }
580
581 var value = s.substring(lastIndex, index).trim();
582 result.add(new _BindingToken(value, isBinding: true));
583 lastIndex = index + 2;
584 }
585 }
586 return result;
587 }
588
589 static void _addTemplateInstanceRecord(fragment, model) {
590 if (fragment.firstChild == null) {
591 return;
592 }
593
594 var instanceRecord = new TemplateInstance(
595 fragment.firstChild, fragment.lastChild, model);
596
597 var node = instanceRecord.firstNode;
598 while (node != null) {
599 node._templateInstance = instanceRecord;
600 node = node.nextNode;
601 }
602 }
603
604 static void _removeAllBindingsRecursively(Node node) {
605 _nodeOrCustom(node).unbindAll();
606 for (var c = node.firstChild; c != null; c = c.nextNode) {
607 _removeAllBindingsRecursively(c);
608 }
609 }
610
611 static void _removeChild(Node parent, Node child) {
612 child._templateInstance = null;
613 if (child is Element && (child as Element).isTemplate) {
614 Element childElement = child;
615 // Make sure we stop observing when we remove an element.
616 var templateIterator = childElement._templateIterator;
617 if (templateIterator != null) {
618 templateIterator.abandon();
619 childElement._templateIterator = null;
620 }
621 }
622 child.remove();
623 _removeAllBindingsRecursively(child);
624 }
625 }
626
627 class _BindingToken {
628 final String value;
629 final bool isBinding;
630
631 _BindingToken(this.value, {this.isBinding: false});
632
633 bool get isText => !isBinding;
634 }
635
636 class _TemplateIterator {
637 final Element _templateElement;
638 final List<Node> terminators = [];
639 final CompoundBinding inputs;
640 List iteratedValue;
641
642 StreamSubscription _sub;
643 StreamSubscription _valueBinding;
644
645 _TemplateIterator(this._templateElement)
646 : inputs = new CompoundBinding(resolveInputs) {
647
648 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged);
649 }
650
651 static Object resolveInputs(Map values) {
652 if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) {
653 return null;
654 }
655
656 if (values.containsKey('repeat')) {
657 return values['repeat'];
658 }
659
660 if (values.containsKey('bind')) {
661 return [values['bind']];
662 }
663
664 return null;
665 }
666
667 void valueChanged(value) {
668 clear();
669 if (value is! List) return;
670
671 iteratedValue = value;
672
673 if (value is Observable) {
674 _sub = value.changes.listen(_handleChanges);
675 }
676
677 int len = iteratedValue.length;
678 if (len > 0) {
679 _handleChanges([new ListChangeRecord(0, addedCount: len)]);
680 }
681 }
682
683 Node getTerminatorAt(int index) {
684 if (index == -1) return _templateElement;
685 var terminator = terminators[index];
686 if (terminator is! Element) return terminator;
687
688 var subIterator = terminator._templateIterator;
689 if (subIterator == null) return terminator;
690
691 return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
692 }
693
694 void insertInstanceAt(int index, Node fragment) {
695 var previousTerminator = getTerminatorAt(index - 1);
696 var terminator = fragment.lastChild;
697 if (terminator == null) terminator = previousTerminator;
698
699 terminators.insert(index, terminator);
700 var parent = _templateElement.parentNode;
701 parent.insertBefore(fragment, previousTerminator.nextNode);
702 }
703
704 void removeInstanceAt(int index) {
705 var previousTerminator = getTerminatorAt(index - 1);
706 var terminator = getTerminatorAt(index);
707 terminators.removeAt(index);
708
709 var parent = _templateElement.parentNode;
710 while (terminator != previousTerminator) {
711 var node = terminator;
712 terminator = node.previousNode;
713 _Bindings._removeChild(parent, node);
714 }
715 }
716
717 void removeAllInstances() {
718 if (terminators.length == 0) return;
719
720 var previousTerminator = _templateElement;
721 var terminator = getTerminatorAt(terminators.length - 1);
722 terminators.length = 0;
723
724 var parent = _templateElement.parentNode;
725 while (terminator != previousTerminator) {
726 var node = terminator;
727 terminator = node.previousNode;
728 _Bindings._removeChild(parent, node);
729 }
730 }
731
732 void clear() {
733 unobserve();
734 removeAllInstances();
735 iteratedValue = null;
736 }
737
738 getInstanceModel(model, syntax) {
739 if (syntax != null) {
740 return syntax.getInstanceModel(_templateElement, model);
741 }
742 return model;
743 }
744
745 getInstanceFragment(syntax) {
746 if (syntax != null) {
747 return syntax.getInstanceFragment(_templateElement);
748 }
749 return _templateElement.createInstance();
750 }
751
752 void _handleChanges(List<ListChangeRecord> splices) {
753 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
754
755 for (var splice in splices) {
756 if (splice is! ListChangeRecord) continue;
757
758 for (int i = 0; i < splice.removedCount; i++) {
759 removeInstanceAt(splice.index);
760 }
761
762 for (var addIndex = splice.index;
763 addIndex < splice.index + splice.addedCount;
764 addIndex++) {
765
766 var model = getInstanceModel(iteratedValue[addIndex], syntax);
767
768 var fragment = getInstanceFragment(syntax);
769
770 _Bindings._addBindings(fragment, model, syntax);
771 _Bindings._addTemplateInstanceRecord(fragment, model);
772
773 insertInstanceAt(addIndex, fragment);
774 }
775 }
776 }
777
778 void unobserve() {
779 if (_sub == null) return;
780 _sub.cancel();
781 _sub = null;
782 }
783
784 void abandon() {
785 unobserve();
786 _valueBinding.cancel();
787 inputs.dispose();
788 }
789 }
OLDNEW
« no previous file with comments | « tools/create_sdk.py ('k') | tools/dom/templates/html/dart2js/html_dart2js.darttemplate » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698