Index: observatory_pub_packages/template_binding/src/template_iterator.dart |
=================================================================== |
--- observatory_pub_packages/template_binding/src/template_iterator.dart (revision 0) |
+++ observatory_pub_packages/template_binding/src/template_iterator.dart (working copy) |
@@ -0,0 +1,556 @@ |
+// 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. |
+ |
+part of template_binding; |
+ |
+// This code is a port of what was formerly known as Model-Driven-Views, now |
+// located at: |
+// https://github.com/polymer/TemplateBinding |
+// https://github.com/polymer/NodeBind |
+ |
+// 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/TemplateBinding/issues/59 |
+bool _toBoolean(value) => null != value && false != value; |
+ |
+// Dart note: this was added to decouple the MustacheTokens.parse function from |
+// the rest of template_binding. |
+_getDelegateFactory(name, node, delegate) { |
+ if (delegate == null) return null; |
+ return (pathString) => delegate.prepareBinding(pathString, name, node); |
+} |
+ |
+_InstanceBindingMap _getBindings(Node node, BindingDelegate delegate) { |
+ if (node is Element) { |
+ return _parseAttributeBindings(node, delegate); |
+ } |
+ |
+ if (node is Text) { |
+ var tokens = MustacheTokens.parse(node.text, |
+ _getDelegateFactory('text', node, delegate)); |
+ if (tokens != null) return new _InstanceBindingMap(['text', tokens]); |
+ } |
+ |
+ return null; |
+} |
+ |
+void _addBindings(Node node, model, [BindingDelegate delegate]) { |
+ final bindings = _getBindings(node, delegate); |
+ if (bindings != null) { |
+ _processBindings(node, bindings, model); |
+ } |
+ |
+ for (var c = node.firstChild; c != null; c = c.nextNode) { |
+ _addBindings(c, model, delegate); |
+ } |
+} |
+ |
+MustacheTokens _parseWithDefault(Element element, String name, |
+ BindingDelegate delegate) { |
+ |
+ var v = element.attributes[name]; |
+ if (v == '') v = '{{}}'; |
+ return MustacheTokens.parse(v, _getDelegateFactory(name, element, delegate)); |
+} |
+ |
+_InstanceBindingMap _parseAttributeBindings(Element element, |
+ BindingDelegate delegate) { |
+ |
+ var bindings = null; |
+ var ifFound = false; |
+ var bindFound = false; |
+ var isTemplateNode = isSemanticTemplate(element); |
+ |
+ element.attributes.forEach((name, value) { |
+ // Allow bindings expressed in attributes to be prefixed with underbars. |
+ // We do this to allow correct semantics for browsers that don't implement |
+ // <template> where certain attributes might trigger side-effects -- and |
+ // for IE which sanitizes certain attributes, disallowing mustache |
+ // replacements in their text. |
+ while (name[0] == '_') { |
+ name = name.substring(1); |
+ } |
+ |
+ if (isTemplateNode && |
+ (name == 'bind' || name == 'if' || name == 'repeat')) { |
+ return; |
+ } |
+ |
+ var tokens = MustacheTokens.parse(value, |
+ _getDelegateFactory(name, element, delegate)); |
+ if (tokens != null) { |
+ if (bindings == null) bindings = []; |
+ bindings..add(name)..add(tokens); |
+ } |
+ }); |
+ |
+ if (isTemplateNode) { |
+ if (bindings == null) bindings = []; |
+ var result = new _TemplateBindingMap(bindings) |
+ .._if = _parseWithDefault(element, 'if', delegate) |
+ .._bind = _parseWithDefault(element, 'bind', delegate) |
+ .._repeat = _parseWithDefault(element, 'repeat', delegate); |
+ |
+ // Treat <template if> as <template bind if> |
+ if (result._if != null && result._bind == null && result._repeat == null) { |
+ result._bind = MustacheTokens.parse('{{}}', |
+ _getDelegateFactory('bind', element, delegate)); |
+ } |
+ |
+ return result; |
+ } |
+ |
+ return bindings == null ? null : new _InstanceBindingMap(bindings); |
+} |
+ |
+_processOneTimeBinding(String name, MustacheTokens tokens, Node node, model) { |
+ |
+ if (tokens.hasOnePath) { |
+ var delegateFn = tokens.getPrepareBinding(0); |
+ var value = delegateFn != null ? delegateFn(model, node, true) : |
+ tokens.getPath(0).getValueFrom(model); |
+ return tokens.isSimplePath ? value : tokens.combinator(value); |
+ } |
+ |
+ // Tokens uses a striding scheme to essentially store a sequence of structs in |
+ // the list. See _MustacheTokens for more information. |
+ var values = new List(tokens.length); |
+ for (int i = 0; i < tokens.length; i++) { |
+ Function delegateFn = tokens.getPrepareBinding(i); |
+ values[i] = delegateFn != null ? |
+ delegateFn(model, node, false) : |
+ tokens.getPath(i).getValueFrom(model); |
+ } |
+ return tokens.combinator(values); |
+} |
+ |
+_processSinglePathBinding(String name, MustacheTokens tokens, Node node, |
+ model) { |
+ Function delegateFn = tokens.getPrepareBinding(0); |
+ var observer = delegateFn != null ? |
+ delegateFn(model, node, false) : |
+ new PathObserver(model, tokens.getPath(0)); |
+ |
+ 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 = 0; i < tokens.length; i++) { |
+ bool oneTime = tokens.getOneTime(i); |
+ Function delegateFn = tokens.getPrepareBinding(i); |
+ |
+ if (delegateFn != null) { |
+ var value = delegateFn(model, node, oneTime); |
+ if (oneTime) { |
+ observer.addPath(value); |
+ } else { |
+ observer.addObserver(value); |
+ } |
+ continue; |
+ } |
+ |
+ PropertyPath path = tokens.getPath(i); |
+ if (oneTime) { |
+ observer.addPath(path.getValueFrom(model)); |
+ } else { |
+ observer.addPath(model, path); |
+ } |
+ } |
+ |
+ return new ObserverTransform(observer, tokens.combinator); |
+} |
+ |
+void _processBindings(Node node, _InstanceBindingMap map, model, |
+ [List<Bindable> instanceBindings]) { |
+ |
+ final bindings = map.bindings; |
+ final nodeExt = nodeBind(node); |
+ for (var i = 0; i < bindings.length; i += 2) { |
+ var name = bindings[i]; |
+ var tokens = bindings[i + 1]; |
+ |
+ var value = _processBinding(name, tokens, node, model); |
+ var binding = nodeExt.bind(name, value, oneTime: tokens.onlyOneTime); |
+ if (binding != null && instanceBindings != null) { |
+ instanceBindings.add(binding); |
+ } |
+ } |
+ |
+ nodeExt.bindFinished(); |
+ if (map is! _TemplateBindingMap) return; |
+ |
+ final templateExt = nodeBindFallback(node); |
+ templateExt._model = model; |
+ |
+ var iter = templateExt._processBindingDirectives(map); |
+ if (iter != null && instanceBindings != null) { |
+ instanceBindings.add(iter); |
+ } |
+} |
+ |
+ |
+// Note: this doesn't really implement most of Bindable. See: |
+// https://github.com/Polymer/TemplateBinding/issues/147 |
+class _TemplateIterator extends Bindable { |
+ final TemplateBindExtension _templateExt; |
+ |
+ final List<DocumentFragment> _instances = []; |
+ |
+ /** A copy of the last rendered [_presentValue] list state. */ |
+ final List _iteratedValue = []; |
+ |
+ 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; |
+ PrepareInstancePositionChangedFunction _instancePositionChangedFn; |
+ |
+ _TemplateIterator(this._templateExt); |
+ |
+ open(callback) => throw new StateError('binding already opened'); |
+ get value => _value; |
+ |
+ Element get _templateElement => _templateExt._node; |
+ |
+ void _closeDependencies() { |
+ if (_ifValue is Bindable) { |
+ _ifValue.close(); |
+ _ifValue = null; |
+ } |
+ if (_value is Bindable) { |
+ _value.close(); |
+ _value = null; |
+ } |
+ } |
+ |
+ void _updateDependencies(_TemplateBindingMap directives, model) { |
+ _closeDependencies(); |
+ |
+ final template = _templateElement; |
+ |
+ _hasIf = directives._if != null; |
+ _hasRepeat = directives._repeat != null; |
+ |
+ var ifValue = true; |
+ if (_hasIf) { |
+ _ifOneTime = directives._if.onlyOneTime; |
+ _ifValue = _processBinding('if', directives._if, template, model); |
+ ifValue = _ifValue; |
+ |
+ // oneTime if & predicate is false. nothing else to do. |
+ if (_ifOneTime && !_toBoolean(ifValue)) { |
+ _valueChanged(null); |
+ return; |
+ } |
+ |
+ if (!_ifOneTime) { |
+ ifValue = (ifValue as Bindable).open(_updateIfValue); |
+ } |
+ } |
+ |
+ 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); |
+ } |
+ |
+ var value = _value; |
+ if (!_oneTime) { |
+ value = _value.open(_updateIteratedValue); |
+ } |
+ |
+ if (!_toBoolean(ifValue)) { |
+ _valueChanged(null); |
+ return; |
+ } |
+ |
+ _updateValue(value); |
+ } |
+ |
+ /// Gets the updated value of the bind/repeat. This can potentially call |
+ /// user code (if a bindingDelegate is set up) so we try to avoid it if we |
+ /// already have the value in hand (from Observer.open). |
+ Object _getUpdatedValue() { |
+ var value = _value; |
+ // Dart note: x.discardChanges() is x.value in Dart. |
+ if (!_toBoolean(_oneTime)) value = value.value; |
+ return value; |
+ } |
+ |
+ void _updateIfValue(ifValue) { |
+ if (!_toBoolean(ifValue)) { |
+ _valueChanged(null); |
+ return; |
+ } |
+ _updateValue(_getUpdatedValue()); |
+ } |
+ |
+ void _updateIteratedValue(value) { |
+ if (_hasIf) { |
+ var ifValue = _ifValue; |
+ if (!_ifOneTime) ifValue = (ifValue as Bindable).value; |
+ if (!_toBoolean(ifValue)) { |
+ _valueChanged([]); |
+ return; |
+ } |
+ } |
+ |
+ _updateValue(value); |
+ } |
+ |
+ void _updateValue(Object 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 = []; |
+ } |
+ } |
+ |
+ if (identical(value, _iteratedValue)) return; |
+ |
+ _unobserve(); |
+ _presentValue = value; |
+ |
+ 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 _getLastInstanceNode(int index) { |
+ if (index == -1) return _templateElement; |
+ // TODO(jmesserly): we could avoid this expando lookup by caching the |
+ // instance extension instead of the instance. |
+ var instance = _instanceExtension[_instances[index]]; |
+ var terminator = instance._terminator; |
+ if (terminator == null) return _getLastInstanceNode(index - 1); |
+ |
+ if (!isSemanticTemplate(terminator) || |
+ identical(terminator, _templateElement)) { |
+ return terminator; |
+ } |
+ |
+ var subtemplateIterator = templateBindFallback(terminator)._iterator; |
+ if (subtemplateIterator == null) return terminator; |
+ |
+ return subtemplateIterator._getLastTemplateNode(); |
+ } |
+ |
+ Node _getLastTemplateNode() => _getLastInstanceNode(_instances.length - 1); |
+ |
+ void _insertInstanceAt(int index, DocumentFragment fragment) { |
+ var previousInstanceLast = _getLastInstanceNode(index - 1); |
+ var parent = _templateElement.parentNode; |
+ |
+ _instances.insert(index, fragment); |
+ parent.insertBefore(fragment, previousInstanceLast.nextNode); |
+ } |
+ |
+ DocumentFragment _extractInstanceAt(int index) { |
+ var previousInstanceLast = _getLastInstanceNode(index - 1); |
+ var lastNode = _getLastInstanceNode(index); |
+ var parent = _templateElement.parentNode; |
+ var instance = _instances.removeAt(index); |
+ |
+ while (lastNode != previousInstanceLast) { |
+ var node = previousInstanceLast.nextNode; |
+ if (node == lastNode) lastNode = previousInstanceLast; |
+ |
+ instance.append(node..remove()); |
+ } |
+ |
+ return instance; |
+ } |
+ |
+ void _handleSplices(List<ListChangeRecord> splices) { |
+ if (_closed || splices.isEmpty) return; |
+ |
+ final template = _templateElement; |
+ |
+ 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 = |
+ delegate.prepareInstancePositionChanged(template); |
+ } |
+ } |
+ |
+ // Instance Removals. |
+ var instanceCache = new HashMap(equals: identical); |
+ var removeDelta = 0; |
+ for (var splice in splices) { |
+ for (var model in splice.removed) { |
+ var instance = _extractInstanceAt(splice.index + removeDelta); |
+ if (instance != _emptyInstance) { |
+ instanceCache[model] = instance; |
+ } |
+ } |
+ |
+ removeDelta -= splice.addedCount; |
+ } |
+ |
+ for (var splice in splices) { |
+ for (var addIndex = splice.index; |
+ addIndex < splice.index + splice.addedCount; |
+ addIndex++) { |
+ |
+ var model = _iteratedValue[addIndex]; |
+ DocumentFragment instance = instanceCache.remove(model); |
+ if (instance == null) { |
+ try { |
+ if (_instanceModelFn != null) { |
+ model = _instanceModelFn(model); |
+ } |
+ if (model == null) { |
+ instance = _emptyInstance; |
+ } else { |
+ instance = _templateExt.createInstance(model, delegate); |
+ } |
+ } catch (e, s) { |
+ // Dart note: we propagate errors asynchronously here to avoid |
+ // disrupting the rendering flow. This is different than in the JS |
+ // implementation but it should probably be fixed there too. Dart |
+ // hits this case more because non-existing properties in |
+ // [PropertyPath] are treated as errors, while JS treats them as |
+ // null/undefined. |
+ // TODO(sigmund): this should be a synchronous throw when this is |
+ // called from createInstance, but that requires enough refactoring |
+ // that it should be done upstream first. See dartbug.com/17789. |
+ new Completer().completeError(e, s); |
+ instance = _emptyInstance; |
+ } |
+ } |
+ |
+ _insertInstanceAt(addIndex, instance); |
+ } |
+ } |
+ |
+ for (var instance in instanceCache.values) { |
+ _closeInstanceBindings(instance); |
+ } |
+ |
+ if (_instancePositionChangedFn != null) _reportInstancesMoved(splices); |
+ } |
+ |
+ void _reportInstanceMoved(int index) { |
+ var instance = _instances[index]; |
+ if (instance == _emptyInstance) return; |
+ |
+ _instancePositionChangedFn(nodeBind(instance).templateInstance, index); |
+ } |
+ |
+ 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); |
+ index++; |
+ } |
+ } else { |
+ index = splice.index; |
+ } |
+ |
+ while (index < splice.index + splice.addedCount) { |
+ _reportInstanceMoved(index); |
+ index++; |
+ } |
+ |
+ offset += splice.addedCount - splice.removed.length; |
+ } |
+ |
+ if (offset == 0) return; |
+ |
+ var length = _instances.length; |
+ while (index < length) { |
+ _reportInstanceMoved(index); |
+ index++; |
+ } |
+ } |
+ |
+ void _closeInstanceBindings(DocumentFragment instance) { |
+ var bindings = _instanceExtension[instance]._bindings; |
+ for (var binding in bindings) binding.close(); |
+ } |
+ |
+ void _unobserve() { |
+ if (_listSub == null) return; |
+ _listSub.cancel(); |
+ _listSub = null; |
+ } |
+ |
+ void close() { |
+ if (_closed) return; |
+ |
+ _unobserve(); |
+ _instances.forEach(_closeInstanceBindings); |
+ _instances.clear(); |
+ _closeDependencies(); |
+ _templateExt._iterator = null; |
+ _closed = true; |
+ } |
+} |
+ |
+// Dart note: the JavaScript version just puts an expando on the array. |
+class _BoundNodes { |
+ final List<Node> nodes; |
+ final List<Bindable> instanceBindings; |
+ _BoundNodes(this.nodes, this.instanceBindings); |
+} |