 Chromium Code Reviews
 Chromium Code Reviews Issue 132403010:
  big update to observe, template_binding, polymer  (Closed) 
  Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
    
  
    Issue 132403010:
  big update to observe, template_binding, polymer  (Closed) 
  Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart| Index: pkg/template_binding/lib/src/template_iterator.dart | 
| diff --git a/pkg/template_binding/lib/src/template_iterator.dart b/pkg/template_binding/lib/src/template_iterator.dart | 
| index c87306a2bbf59e1c39fa254d95b9a52174ed5c58..e5de32c35a7224ae1f4d21cbe3e44e009827c366 100644 | 
| --- a/pkg/template_binding/lib/src/template_iterator.dart | 
| +++ b/pkg/template_binding/lib/src/template_iterator.dart | 
| @@ -17,23 +17,23 @@ part of template_binding; | 
| // See: https://github.com/polymer/TemplateBinding/issues/59 | 
| bool _toBoolean(value) => null != value && false != value; | 
| -List _getBindings(Node node, BindingDelegate delegate) { | 
| +_InstanceBindingMap _getBindings(Node node, BindingDelegate delegate) { | 
| if (node is Element) { | 
| return _parseAttributeBindings(node, delegate); | 
| } | 
| if (node is Text) { | 
| var tokens = _parseMustaches(node.text, 'text', node, delegate); | 
| - if (tokens != null) return ['text', tokens]; | 
| + if (tokens != null) return new _InstanceBindingMap(['text', tokens]); | 
| } | 
| return null; | 
| } | 
| void _addBindings(Node node, model, [BindingDelegate delegate]) { | 
| - var bindings = _getBindings(node, delegate); | 
| + final bindings = _getBindings(node, delegate); | 
| if (bindings != null) { | 
| - _processBindings(bindings, node, model); | 
| + _processBindings(node, bindings, model); | 
| } | 
| for (var c = node.firstChild; c != null; c = c.nextNode) { | 
| @@ -41,8 +41,17 @@ void _addBindings(Node node, model, [BindingDelegate delegate]) { | 
| } | 
| } | 
| +_MustacheTokens _parseWithDefault(Element element, String name, | 
| + BindingDelegate delegate) { | 
| + | 
| + var v = element.attributes[name]; | 
| + if (v == '') v = '{{}}'; | 
| + return _parseMustaches(v, name, element, delegate); | 
| +} | 
| + | 
| +_InstanceBindingMap _parseAttributeBindings(Element element, | 
| + BindingDelegate delegate) { | 
| -List _parseAttributeBindings(Element element, BindingDelegate delegate) { | 
| var bindings = null; | 
| var ifFound = false; | 
| var bindFound = false; | 
| @@ -58,14 +67,9 @@ List _parseAttributeBindings(Element element, BindingDelegate delegate) { | 
| name = name.substring(1); | 
| } | 
| - if (isTemplateNode) { | 
| - if (name == 'if') { | 
| - ifFound = true; | 
| - if (value == '') value = '{{}}'; // Accept 'naked' if. | 
| - } else if (name == 'bind' || name == 'repeat') { | 
| - bindFound = true; | 
| - if (value == '') value = '{{}}'; // Accept 'naked' bind & repeat. | 
| - } | 
| + if (isTemplateNode && | 
| + (name == 'bind' || name == 'if' || name == 'repeat')) { | 
| + return; | 
| } | 
| var tokens = _parseMustaches(value, name, element, delegate); | 
| @@ -75,63 +79,111 @@ List _parseAttributeBindings(Element element, BindingDelegate delegate) { | 
| } | 
| }); | 
| - // Treat <template if> as <template bind if> | 
| - if (ifFound && !bindFound) { | 
| + if (isTemplateNode) { | 
| if (bindings == null) bindings = []; | 
| - bindings..add('bind') | 
| - ..add(_parseMustaches('{{}}', 'bind', element, delegate)); | 
| + var result = new _TemplateBindingMap(bindings) | 
| + .._if = _parseWithDefault(element, 'if', delegate) | 
| + .._bind = _parseWithDefault(element, 'bind', delegate) | 
| + .._repeat = _parseWithDefault(element, 'repeat', delegate); | 
| + | 
| + if (result._if != null && result._bind == null && result._repeat == null) { | 
| 
justinfagnani
2014/02/04 20:16:08
why is this necessary?
 
Jennifer Messerly
2014/02/04 23:00:32
good point! This used to have a comment, not sure
 | 
| + result._bind = _parseMustaches('{{}}', 'bind', element, delegate); | 
| + } | 
| + | 
| + return result; | 
| } | 
| - return bindings; | 
| + return bindings == null ? null : new _InstanceBindingMap(bindings); | 
| } | 
| -void _processBindings(List bindings, Node node, model, | 
| - [List<NodeBinding> bound]) { | 
| +_processOneTimeBinding(String name, _MustacheTokens tokens, Node node, model) { | 
| - for (var i = 0; i < bindings.length; i += 2) { | 
| - var name = bindings[i]; | 
| - var tokens = bindings[i + 1]; | 
| - var bindingModel = model; | 
| - var bindingPath = tokens.tokens[1]; | 
| - if (tokens.hasOnePath) { | 
| - var delegateFn = tokens.tokens[2]; | 
| - if (delegateFn != null) { | 
| - var delegateBinding = delegateFn(model, node); | 
| - if (delegateBinding != null) { | 
| - bindingModel = delegateBinding; | 
| - bindingPath = 'value'; | 
| - } | 
| - } | 
| + if (tokens.hasOnePath) { | 
| + var delegateFn = tokens.tokens[3]; | 
| + var value = delegateFn != null ? delegateFn(model, node, true) : | 
| + tokens.tokens[2].getValueFrom(model); | 
| + return tokens.isSimplePath ? value : tokens._combinator(value); | 
| + } | 
| + | 
| + var values = new List(tokens.tokens.length ~/ 4); | 
| 
justinfagnani
2014/02/04 20:16:08
oliver twist voice: more comments please?
 
Jennifer Messerly
2014/02/04 23:00:32
hah, you and me both want this :)
I'll try and cl
 | 
| + for (int i = 1; i < tokens.tokens.length; i += 4) { | 
| + Function delegateFn = tokens.tokens[i + 2]; | 
| 
justinfagnani
2014/02/04 20:16:08
I'd love named constants for the offsets
 
Jennifer Messerly
2014/02/04 23:00:32
Excellent idea! I've attempted to encapsulate the
 | 
| + values[(i - 1) ~/ 4] = delegateFn != null ? | 
| + delegateFn(model, node, false) : | 
| + tokens.tokens[i + 1].getValueFrom(model); | 
| + } | 
| + return tokens._combinator(values); | 
| +} | 
| + | 
| +_processSinglePathBinding(String name, _MustacheTokens tokens, Node node, | 
| + model) { | 
| + Function delegateFn = tokens.tokens[3]; | 
| + var observer = delegateFn != null ? | 
| + delegateFn(model, node, false) : | 
| + new PathObserver(model, tokens.tokens[2]); | 
| + | 
| + return tokens.isSimplePath ? observer : | 
| + new ObserverTransform(observer, tokens._combinator); | 
| +} | 
| + | 
| +_processBinding(String name, _MustacheTokens tokens, Node node, model) { | 
| + if (tokens.onlyOneTime) { | 
| + return _processOneTimeBinding(name, tokens, node, model); | 
| + } | 
| + if (tokens.hasOnePath) { | 
| + return _processSinglePathBinding(name, tokens, node, model); | 
| + } | 
| + | 
| + var observer = new CompoundObserver(); | 
| + | 
| + for (int i = 1; i < tokens.tokens.length; i += 4) { | 
| + bool oneTime = tokens.tokens[i]; | 
| + Function delegateFn = tokens.tokens[i + 2]; | 
| - if (!tokens.isSimplePath) { | 
| - bindingModel = new PathObserver(bindingModel, bindingPath, | 
| - computeValue: tokens.combinator); | 
| - bindingPath = 'value'; | 
| + if (delegateFn != null) { | 
| + var value = delegateFn(model, node, oneTime); | 
| + if (oneTime) { | 
| + observer.addPath(value); | 
| + } else { | 
| + observer.addObserver(value); | 
| } | 
| + continue; | 
| + } | 
| + | 
| + PropertyPath path = tokens.tokens[i + 1]; | 
| + if (oneTime) { | 
| + observer.addPath(path.getValueFrom(model)); | 
| } else { | 
| - var observer = new CompoundPathObserver(computeValue: tokens.combinator); | 
| - for (var j = 1; j < tokens.tokens.length; j += 3) { | 
| - var subModel = model; | 
| - var subPath = tokens.tokens[j]; | 
| - var delegateFn = tokens.tokens[j + 1]; | 
| - var delegateBinding = delegateFn != null ? | 
| - delegateFn(subModel, node) : null; | 
| - | 
| - if (delegateBinding != null) { | 
| - subModel = delegateBinding; | 
| - subPath = 'value'; | 
| - } | 
| + observer.addPath(model, path); | 
| + } | 
| + } | 
| - observer.addPath(subModel, subPath); | 
| - } | 
| + return new ObserverTransform(observer, tokens._combinator); | 
| +} | 
| + | 
| +void _processBindings(Node node, _InstanceBindingMap map, model, | 
| + [List<Bindable> instanceBindings]) { | 
| + | 
| + final bindings = map.bindings; | 
| + for (var i = 0; i < bindings.length; i += 2) { | 
| + var name = bindings[i]; | 
| + var tokens = bindings[i + 1]; | 
| - observer.start(); | 
| - bindingModel = observer; | 
| - bindingPath = 'value'; | 
| + var value = _processBinding(name, tokens, node, model); | 
| + var binding = nodeBind(node).bind(name, value, oneTime: tokens.onlyOneTime); | 
| + if (binding != null && instanceBindings != null) { | 
| + instanceBindings.add(binding); | 
| } | 
| + } | 
| + | 
| + if (map is! _TemplateBindingMap) return; | 
| - var binding = nodeBind(node).bind(name, bindingModel, bindingPath); | 
| - if (bound != null) bound.add(binding); | 
| + final templateExt = nodeBindFallback(node); | 
| + templateExt._model = model; | 
| + | 
| + var iter = templateExt._processBindingDirectives(map); | 
| + if (iter != null && instanceBindings != null) { | 
| + instanceBindings.add(iter); | 
| } | 
| } | 
| @@ -142,14 +194,26 @@ void _processBindings(List bindings, Node node, model, | 
| */ | 
| _MustacheTokens _parseMustaches(String s, String name, Node node, | 
| BindingDelegate delegate) { | 
| - if (s.isEmpty) return null; | 
| + if (s == null || s.isEmpty) return null; | 
| var tokens = null; | 
| var length = s.length; | 
| - var startIndex = 0, lastIndex = 0, endIndex = 0; | 
| + var lastIndex = 0; | 
| + var onlyOneTime = true; | 
| while (lastIndex < length) { | 
| - startIndex = s.indexOf('{{', lastIndex); | 
| - endIndex = startIndex < 0 ? -1 : s.indexOf('}}', startIndex + 2); | 
| + var startIndex = s.indexOf('{{', lastIndex); | 
| + var oneTimeStart = s.indexOf('[[', lastIndex); | 
| + var oneTime = false; | 
| + var terminator = '}}'; | 
| + | 
| + if (oneTimeStart >= 0 && | 
| + (startIndex < 0 || oneTimeStart < startIndex)) { | 
| + startIndex = oneTimeStart; | 
| + oneTime = true; | 
| + terminator = ']]'; | 
| + } | 
| + | 
| + var endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2); | 
| if (endIndex < 0) { | 
| if (tokens == null) return null; | 
| @@ -161,7 +225,9 @@ _MustacheTokens _parseMustaches(String s, String name, Node node, | 
| if (tokens == null) tokens = []; | 
| tokens.add(s.substring(lastIndex, startIndex)); // TEXT | 
| var pathString = s.substring(startIndex + 2, endIndex).trim(); | 
| - tokens.add(pathString); // PATH | 
| + tokens.add(oneTime); // ONETIME? | 
| + onlyOneTime = onlyOneTime && oneTime; | 
| + tokens.add(new PropertyPath(pathString)); // PATH | 
| var delegateFn = delegate == null ? null : | 
| delegate.prepareBinding(pathString, name, node); | 
| tokens.add(delegateFn); | 
| @@ -171,25 +237,30 @@ _MustacheTokens _parseMustaches(String s, String name, Node node, | 
| if (lastIndex == length) tokens.add(''); | 
| - return new _MustacheTokens(tokens); | 
| + return new _MustacheTokens(tokens, onlyOneTime); | 
| } | 
| class _MustacheTokens { | 
| - bool get hasOnePath => tokens.length == 4; | 
| - bool get isSimplePath => hasOnePath && tokens[0] == '' && tokens[3] == ''; | 
| + bool get hasOnePath => tokens.length == 5; | 
| + bool get isSimplePath => hasOnePath && tokens[0] == '' && tokens[4] == ''; | 
| - /** [TEXT, (PATH, TEXT, DELEGATE_FN)+] if there is at least one mustache. */ | 
| + /** | 
| + * [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least one | 
| + * mustache. | 
| + */ | 
| // TODO(jmesserly): clean up the type here? | 
| final List tokens; | 
| + final bool onlyOneTime; | 
| + | 
| // Dart note: I think this is cached in JavaScript to avoid an extra | 
| // allocation per template instance. Seems reasonable, so we do the same. | 
| Function _combinator; | 
| Function get combinator => _combinator; | 
| - _MustacheTokens(this.tokens) { | 
| - // Should be: [TEXT, (PATH, TEXT, DELEGATE_FN)+]. | 
| - assert((tokens.length + 2) % 3 == 0); | 
| + _MustacheTokens(this.tokens, this.onlyOneTime) { | 
| + // Should be: [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+]. | 
| + assert((tokens.length - 1) % 4 == 0); | 
| _combinator = hasOnePath ? _singleCombinator : _listCombinator; | 
| } | 
| @@ -198,53 +269,49 @@ class _MustacheTokens { | 
| // argument can be typed. | 
| String _singleCombinator(Object value) { | 
| if (value == null) value = ''; | 
| - return '${tokens[0]}$value${tokens[3]}'; | 
| + return '${tokens[0]}$value${tokens[4]}'; | 
| } | 
| String _listCombinator(List<Object> values) { | 
| var newValue = new StringBuffer(tokens[0]); | 
| - for (var i = 1; i < tokens.length; i += 3) { | 
| - var value = values[(i - 1) ~/ 3]; | 
| + for (var i = 1; i < tokens.length; i += 4) { | 
| + var value = values[(i - 1) ~/ 4]; | 
| if (value != null) newValue.write(value); | 
| - newValue.write(tokens[i + 2]); | 
| + newValue.write(tokens[i + 3]); | 
| } | 
| return newValue.toString(); | 
| } | 
| } | 
| -void _addTemplateInstanceRecord(fragment, model) { | 
| - if (fragment.firstChild == null) { | 
| - return; | 
| - } | 
| - | 
| - var instanceRecord = new TemplateInstance( | 
| - fragment.firstChild, fragment.lastChild, model); | 
| - | 
| - var node = instanceRecord.firstNode; | 
| - while (node != null) { | 
| - nodeBindFallback(node)._templateInstance = instanceRecord; | 
| - node = node.nextNode; | 
| - } | 
| -} | 
| -class _TemplateIterator { | 
| +// Note: this doesn't really implement most of Bindable. See: | 
| +// https://github.com/Polymer/TemplateBinding/issues/147 | 
| +class _TemplateIterator extends Bindable { | 
| final TemplateBindExtension _templateExt; | 
| /** | 
| * Flattened array of tuples: | 
| * <instanceTerminatorNode, [bindingsSetupByInstance]> | 
| */ | 
| - final List terminators = []; | 
| - List iteratedValue; | 
| - bool closed = false; | 
| - bool depsChanging = false; | 
| + final List _terminators = []; | 
| - bool hasRepeat = false, hasBind = false, hasIf = false; | 
| - Object repeatModel, bindModel, ifModel; | 
| - String repeatPath, bindPath, ifPath; | 
| + /** A copy of the last rendered [_presentValue] list state. */ | 
| + final List _iteratedValue = []; | 
| - StreamSubscription _valueSub, _listSub; | 
| + List _presentValue; | 
| + | 
| + bool _closed = false; | 
| + | 
| + // Dart note: instead of storing these in a Map like JS, or using a separate | 
| + // object (extra memory overhead) we just inline the fields. | 
| + var _ifValue, _value; | 
| + | 
| + // TODO(jmesserly): lots of booleans in this object. Bitmask? | 
| + bool _hasIf, _hasRepeat; | 
| + bool _ifOneTime, _oneTime; | 
| + | 
| + StreamSubscription _listSub; | 
| bool _initPrepareFunctions = false; | 
| PrepareInstanceModelFunction _instanceModelFn; | 
| @@ -252,81 +319,106 @@ class _TemplateIterator { | 
| _TemplateIterator(this._templateExt); | 
| - Element get _templateElement => _templateExt._node; | 
| + open(callback) => throw new StateError('binding already opened'); | 
| + get value => _value; | 
| - resolve() { | 
| - depsChanging = false; | 
| + Element get _templateElement => _templateExt._node; | 
| - if (_valueSub != null) { | 
| - _valueSub.cancel(); | 
| - _valueSub = null; | 
| + void _closeDependencies() { | 
| + if (_ifValue is Bindable) { | 
| + _ifValue.close(); | 
| + _ifValue = null; | 
| } | 
| - | 
| - if (!hasRepeat && !hasBind) { | 
| - _valueChanged(null); | 
| - return; | 
| + if (_value is Bindable) { | 
| + _value.close(); | 
| + _value = null; | 
| } | 
| + } | 
| - final model = hasRepeat ? repeatModel : bindModel; | 
| - final path = hasRepeat ? repeatPath : bindPath; | 
| + void _updateDependencies(_TemplateBindingMap directives, model) { | 
| + _closeDependencies(); | 
| - var valueObserver; | 
| - if (!hasIf) { | 
| - valueObserver = new PathObserver(model, path, | 
| - computeValue: hasRepeat ? null : (x) => [x]); | 
| - } else { | 
| - // TODO(jmesserly): I'm not sure if closing over this is necessary for | 
| - // correctness. It does seem useful if the valueObserver gets fired after | 
| - // hasRepeat has changed, due to async nature of things. | 
| - final isRepeat = hasRepeat; | 
| - | 
| - valueFn(List values) { | 
| - var modelValue = values[0]; | 
| - var ifValue = values[1]; | 
| - if (!_toBoolean(ifValue)) return null; | 
| - return isRepeat ? modelValue : [ modelValue ]; | 
| + final template = _templateElement; | 
| + | 
| + _hasIf = directives._if != null; | 
| + _hasRepeat = directives._repeat != null; | 
| + | 
| + if (_hasIf) { | 
| + _ifOneTime = directives._if.onlyOneTime; | 
| + _ifValue = _processBinding('if', directives._if, template, model); | 
| + | 
| + // oneTime if & predicate is false. nothing else to do. | 
| + if (_ifOneTime) { | 
| + if (!_toBoolean(_ifValue)) { | 
| + _updateIteratedValue(null); | 
| + return; | 
| + } | 
| + } else { | 
| + (_ifValue as Bindable).open(_updateIteratedValue); | 
| } | 
| + } | 
| - valueObserver = new CompoundPathObserver(computeValue: valueFn) | 
| - ..addPath(model, path) | 
| - ..addPath(ifModel, ifPath) | 
| - ..start(); | 
| + if (_hasRepeat) { | 
| + _oneTime = directives._repeat.onlyOneTime; | 
| + _value = _processBinding('repeat', directives._repeat, template, model); | 
| + } else { | 
| + _oneTime = directives._bind.onlyOneTime; | 
| + _value = _processBinding('bind', directives._bind, template, model); | 
| } | 
| - _valueSub = valueObserver.changes.listen( | 
| - (r) => _valueChanged(r.last.newValue)); | 
| - _valueChanged(valueObserver.value); | 
| + if (!_oneTime) _value.open(_updateIteratedValue); | 
| + | 
| + _updateIteratedValue(null); | 
| } | 
| - void _valueChanged(newValue) { | 
| - var oldValue = iteratedValue; | 
| - unobserve(); | 
| - | 
| - if (newValue is List) { | 
| - iteratedValue = newValue; | 
| - } else if (newValue is Iterable) { | 
| - // Dart note: we support Iterable by calling toList. | 
| - // But we need to be careful to observe the original iterator if it | 
| - // supports that. | 
| - iteratedValue = (newValue as Iterable).toList(); | 
| - } else { | 
| - iteratedValue = null; | 
| + void _updateIteratedValue(_) { | 
| + if (_hasIf) { | 
| + var ifValue = _ifValue; | 
| + if (!_ifOneTime) ifValue = (ifValue as Bindable).value; | 
| + if (!_toBoolean(ifValue)) { | 
| + _valueChanged([]); | 
| + return; | 
| + } | 
| } | 
| - if (iteratedValue != null && newValue is ObservableList) { | 
| - _listSub = newValue.listChanges.listen(_handleSplices); | 
| + var value = _value; | 
| + if (!_oneTime) value = (value as Bindable).value; | 
| + if (!_hasRepeat) value = [value]; | 
| + _valueChanged(value); | 
| + } | 
| + | 
| + void _valueChanged(Object value) { | 
| + if (value is! List) { | 
| + if (value is Iterable) { | 
| + // Dart note: we support Iterable by calling toList. | 
| + // But we need to be careful to observe the original iterator if it | 
| + // supports that. | 
| + value = (value as Iterable).toList(); | 
| + } else { | 
| + value = []; | 
| + } | 
| } | 
| - var splices = ObservableList.calculateChangeRecords( | 
| - oldValue != null ? oldValue : [], | 
| - iteratedValue != null ? iteratedValue : []); | 
| + if (identical(value, _iteratedValue)) return; | 
| + | 
| + _unobserve(); | 
| + _presentValue = value; | 
| - if (splices.isNotEmpty) _handleSplices(splices); | 
| + if (value is ObservableList && _hasRepeat && !_oneTime) { | 
| + // Make sure any pending changes aren't delivered, since we're getting | 
| + // a snapshot at this point in time. | 
| + value.discardListChages(); | 
| + _listSub = value.listChanges.listen(_handleSplices); | 
| + } | 
| + | 
| + _handleSplices(ObservableList.calculateChangeRecords( | 
| + _iteratedValue != null ? _iteratedValue : [], | 
| + _presentValue != null ? _presentValue : [])); | 
| } | 
| - Node getTerminatorAt(int index) { | 
| + Node _getTerminatorAt(int index) { | 
| if (index == -1) return _templateElement; | 
| - var terminator = terminators[index * 2]; | 
| + var terminator = _terminators[index * 2]; | 
| if (!isSemanticTemplate(terminator) || | 
| identical(terminator, _templateElement)) { | 
| return terminator; | 
| @@ -335,15 +427,15 @@ class _TemplateIterator { | 
| var subIter = templateBindFallback(terminator)._iterator; | 
| if (subIter == null) return terminator; | 
| - return subIter.getTerminatorAt(subIter.terminators.length ~/ 2 - 1); | 
| + return subIter._getTerminatorAt(subIter._terminators.length ~/ 2 - 1); | 
| } | 
| // TODO(rafaelw): If we inserting sequences of instances we can probably | 
| - // avoid lots of calls to getTerminatorAt(), or cache its result. | 
| - void insertInstanceAt(int index, DocumentFragment fragment, | 
| - List<Node> instanceNodes, List<NodeBinding> bound) { | 
| + // avoid lots of calls to _getTerminatorAt(), or cache its result. | 
| + void _insertInstanceAt(int index, DocumentFragment fragment, | 
| + List<Node> instanceNodes, List<Bindable> instanceBindings) { | 
| - var previousTerminator = getTerminatorAt(index - 1); | 
| + var previousTerminator = _getTerminatorAt(index - 1); | 
| var terminator = null; | 
| if (fragment != null) { | 
| terminator = fragment.lastChild; | 
| @@ -352,7 +444,7 @@ class _TemplateIterator { | 
| } | 
| if (terminator == null) terminator = previousTerminator; | 
| - terminators.insertAll(index * 2, [terminator, bound]); | 
| + _terminators.insertAll(index * 2, [terminator, instanceBindings]); | 
| var parent = _templateElement.parentNode; | 
| var insertBeforeNode = previousTerminator.nextNode; | 
| @@ -365,12 +457,12 @@ class _TemplateIterator { | 
| } | 
| } | 
| - _BoundNodes extractInstanceAt(int index) { | 
| + _BoundNodes _extractInstanceAt(int index) { | 
| var instanceNodes = <Node>[]; | 
| - var previousTerminator = getTerminatorAt(index - 1); | 
| - var terminator = getTerminatorAt(index); | 
| - var bound = terminators[index * 2 + 1]; | 
| - terminators.removeRange(index * 2, index * 2 + 2); | 
| + var previousTerminator = _getTerminatorAt(index - 1); | 
| + var terminator = _getTerminatorAt(index); | 
| + var instanceBindings = _terminators[index * 2 + 1]; | 
| + _terminators.removeRange(index * 2, index * 2 + 2); | 
| var parent = _templateElement.parentNode; | 
| while (terminator != previousTerminator) { | 
| @@ -379,24 +471,28 @@ class _TemplateIterator { | 
| node.remove(); | 
| instanceNodes.add(node); | 
| } | 
| - return new _BoundNodes(instanceNodes, bound); | 
| + return new _BoundNodes(instanceNodes, instanceBindings); | 
| } | 
| void _handleSplices(List<ListChangeRecord> splices) { | 
| - if (closed) return; | 
| + if (_closed || splices.isEmpty) return; | 
| final template = _templateElement; | 
| - final delegate = _templateExt._self.bindingDelegate; | 
| - if (template.parentNode == null || template.ownerDocument.window == null) { | 
| + if (template.parentNode == null) { | 
| close(); | 
| return; | 
| } | 
| + ObservableList.applyChangeRecords(_iteratedValue, _presentValue, splices); | 
| + | 
| + final delegate = _templateExt.bindingDelegate; | 
| + | 
| // Dart note: the JavaScript code relies on the distinction between null | 
| // and undefined to track whether the functions are prepared. We use a bool. | 
| if (!_initPrepareFunctions) { | 
| _initPrepareFunctions = true; | 
| + final delegate = _templateExt._self.bindingDelegate; | 
| if (delegate != null) { | 
| _instanceModelFn = delegate.prepareInstanceModel(template); | 
| _instancePositionChangedFn = | 
| @@ -408,7 +504,7 @@ class _TemplateIterator { | 
| var removeDelta = 0; | 
| for (var splice in splices) { | 
| for (var model in splice.removed) { | 
| - instanceCache[model] = extractInstanceAt(splice.index + removeDelta); | 
| + instanceCache[model] = _extractInstanceAt(splice.index + removeDelta); | 
| } | 
| removeDelta -= splice.addedCount; | 
| @@ -419,38 +515,39 @@ class _TemplateIterator { | 
| addIndex < splice.index + splice.addedCount; | 
| addIndex++) { | 
| - var model = iteratedValue[addIndex]; | 
| + var model = _iteratedValue[addIndex]; | 
| var fragment = null; | 
| var instance = instanceCache.remove(model); | 
| - List bound; | 
| + List instanceBindings; | 
| List instanceNodes = null; | 
| if (instance != null && instance.nodes.isNotEmpty) { | 
| - bound = instance.bound; | 
| + instanceBindings = instance.instanceBindings; | 
| instanceNodes = instance.nodes; | 
| } else { | 
| - bound = []; | 
| + instanceBindings = []; | 
| if (_instanceModelFn != null) { | 
| model = _instanceModelFn(model); | 
| } | 
| if (model != null) { | 
| - fragment = _templateExt.createInstance(model, delegate, bound); | 
| + fragment = _templateExt.createInstance(model, delegate, | 
| + instanceBindings); | 
| } | 
| } | 
| - insertInstanceAt(addIndex, fragment, instanceNodes, bound); | 
| + _insertInstanceAt(addIndex, fragment, instanceNodes, instanceBindings); | 
| } | 
| } | 
| for (var instance in instanceCache.values) { | 
| - closeInstanceBindings(instance.bound); | 
| + _closeInstanceBindings(instance.instanceBindings); | 
| } | 
| - if (_instancePositionChangedFn != null) reportInstancesMoved(splices); | 
| + if (_instancePositionChangedFn != null) _reportInstancesMoved(splices); | 
| } | 
| - void reportInstanceMoved(int index) { | 
| - var previousTerminator = getTerminatorAt(index - 1); | 
| - var terminator = getTerminatorAt(index); | 
| + void _reportInstanceMoved(int index) { | 
| + var previousTerminator = _getTerminatorAt(index - 1); | 
| + var terminator = _getTerminatorAt(index); | 
| if (identical(previousTerminator, terminator)) { | 
| return; // instance has zero nodes. | 
| } | 
| @@ -463,13 +560,13 @@ class _TemplateIterator { | 
| _instancePositionChangedFn(instance, index); | 
| } | 
| - void reportInstancesMoved(List<ListChangeRecord> splices) { | 
| + void _reportInstancesMoved(List<ListChangeRecord> splices) { | 
| var index = 0; | 
| var offset = 0; | 
| for (var splice in splices) { | 
| if (offset != 0) { | 
| while (index < splice.index) { | 
| - reportInstanceMoved(index); | 
| + _reportInstanceMoved(index); | 
| index++; | 
| } | 
| } else { | 
| @@ -477,7 +574,7 @@ class _TemplateIterator { | 
| } | 
| while (index < splice.index + splice.addedCount) { | 
| - reportInstanceMoved(index); | 
| + _reportInstanceMoved(index); | 
| index++; | 
| } | 
| @@ -486,44 +583,41 @@ class _TemplateIterator { | 
| if (offset == 0) return; | 
| - var length = terminators.length ~/ 2; | 
| + var length = _terminators.length ~/ 2; | 
| while (index < length) { | 
| - reportInstanceMoved(index); | 
| + _reportInstanceMoved(index); | 
| index++; | 
| } | 
| } | 
| - void closeInstanceBindings(List<NodeBinding> bound) { | 
| - for (var binding in bound) binding.close(); | 
| + void _closeInstanceBindings(List<Bindable> instanceBindings) { | 
| + for (var binding in instanceBindings) binding.close(); | 
| } | 
| - void unobserve() { | 
| + void _unobserve() { | 
| if (_listSub == null) return; | 
| _listSub.cancel(); | 
| _listSub = null; | 
| } | 
| void close() { | 
| - if (closed) return; | 
| + if (_closed) return; | 
| - unobserve(); | 
| - for (var i = 1; i < terminators.length; i += 2) { | 
| - closeInstanceBindings(terminators[i]); | 
| + _unobserve(); | 
| + for (var i = 1; i < _terminators.length; i += 2) { | 
| + _closeInstanceBindings(_terminators[i]); | 
| } | 
| - terminators.clear(); | 
| - if (_valueSub != null) { | 
| - _valueSub.cancel(); | 
| - _valueSub = null; | 
| - } | 
| + _terminators.clear(); | 
| + _closeDependencies(); | 
| _templateExt._iterator = null; | 
| - closed = true; | 
| + _closed = true; | 
| } | 
| } | 
| // Dart note: the JavaScript version just puts an expando on the array. | 
| class _BoundNodes { | 
| final List<Node> nodes; | 
| - final List<NodeBinding> bound; | 
| - _BoundNodes(this.nodes, this.bound); | 
| + final List<Bindable> instanceBindings; | 
| + _BoundNodes(this.nodes, this.instanceBindings); | 
| } |