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

Unified Diff: tools/dom/src/TemplateBindings.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: trying upload again Created 7 years, 8 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 side-by-side diff with in-line comments
Download patch
Index: tools/dom/src/TemplateBindings.dart
diff --git a/tools/dom/src/TemplateBindings.dart b/tools/dom/src/TemplateBindings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..955671b8998edacec0548ae5605abf759fb4bee6
--- /dev/null
+++ b/tools/dom/src/TemplateBindings.dart
@@ -0,0 +1,792 @@
+// 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 html;
+
+// This code is a port of Model-Driven-Views:
+// https://github.com/toolkitchen/mdv
+// The code mostly comes from src/template_element.js
Jennifer Messerly 2013/05/01 00:44:58 What's up with JavaStyle file names in tools/dom/s
blois 2013/05/01 17:00:42 We should rename en-masse. It keeps on bugging me,
Jennifer Messerly 2013/05/02 02:58:33 sounds good to me
+
+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/toolkitchen/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 {
+ // 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);
+}
+
+/** 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.
+// TODO(jmesserly): should this move into dart:observe?
+@Experimental
+class CompoundBinding extends ObservableMixin {
+ CompoundBindingCombinator _combinator;
+
+ // TODO(jmesserly): ideally these would be String keys, but sometimes we
+ // use integers.
+ Map<dynamic, _Binding> _bindings = {};
+ Map _values = {};
+ 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 = 'value';
+
+ get value => _value;
+
+ void set value(newValue) {
+ _value = notifyChange('value', _value, newValue);
+ }
+
+ // TODO(jmesserly): remove getValue/setValue when dart2js supports mirrors!
+ getValue(key) {
+ if (key == _VALUE) return value;
+ return null;
+ }
+ setValue(key, val) {
+ if (key == _VALUE) value = val;
+ }
+
+ void bind(name, model, String path) {
+ unbind(name);
+
+ _bindings[name] = new _Binding(model, path, (value) {
+ _values[name] = value;
+ _scheduleResolve();
+ });
+ }
+
+ void unbind(name, {bool suppressResolve: false}) {
+ var binding = _bindings.remove(name);
+ if (binding == null) return;
+
+ binding.dispose();
+ _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.dispose();
+ }
+ _bindings.clear();
+ _values.clear();
+
+ _disposed = true;
+ value = null;
+ }
+}
+
+// TODO(jmesserly): we can probably inline this object as it doesn't do much.
+class _Binding {
+ final _ChangeHandler changed;
+ final PathObserver _path;
+ StreamSubscription _sub;
+
+ _Binding(model, String path, _ChangeHandler changed)
+ : _path = observePath(model, path) {
+ if (_path == null) {
+ // TODO(jmesserly): Should we display an error message about invalid path?
+ changed(null);
+ return;
+ }
+ _sub = _path.values.listen(changed);
+ changed(_path.value);
+ }
+
+ void dispose() {
+ if (_sub != null) _sub.cancel();
+ }
+
+ void set value(newValue) {
+ if (_path != null) _path.value = newValue;
+ }
+}
+
+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;
+ }
+}
+
+abstract class _InputBinding {
+ final InputElement element;
+ _Binding binding;
+ StreamSubscription _sub;
+
+ _InputBinding(this.element, model, String path) {
+ binding = new _Binding(model, path, valueChanged);
+ _sub = _getStreamForInputType(element).listen(updateBinding);
+ }
+
+ void valueChanged(newValue);
+
+ void updateBinding(e);
+
+ void unbind() {
+ binding.dispose();
+ _sub.cancel();
+ }
+}
+
+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;
+ }
+}
+
+// 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/toolkitchen/mdv/issues/59
+bool _templateBooleanConversion(value) => null != value && false != value;
+
+class _CheckedBinding extends _InputBinding {
+ _CheckedBinding(element, model, path) : super(element, model, path);
+
+ void valueChanged(value) {
+ element.checked = _templateBooleanConversion(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;
+ }
+ }
+ }
+ }
+}
+
+// TODO(jmesserly): do we need to polyfill document.contains for IE?
+bool _isNodeInDocument(Node node) => node.document.contains(node);
+
+// |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
+//
+Iterable<Element> _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);
+ }
+}
+
+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.nodes.add(_createDeepCloneAndDecorateTemplates(c, syntax));
+ }
+ return clone;
+}
+
+// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner
+Document _getTemplateContentsOwner(Document 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;
+}
+
+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;
+}
+
+void _liftNonNativeTemplateChildrenIntoContent(Element templateElement) {
+ var content = templateElement.content;
+
+ if (!templateElement._isAttributeTemplate) {
+ var child;
+ while ((child = templateElement.$dom_firstChild) != null) {
+ content.nodes.add(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.nodes.add(child);
+ }
+ content.nodes.add(newRoot);
+}
+
+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 templateDescendents = (node as dynamic).queryAll(_allTemplatesSelectors);
+ if (node is Element && node.isTemplate) bootstrap(node);
+
+ templateDescendents.forEach(bootstrap);
+}
+
+final String _allTemplatesSelectors = 'template, option[template], ' +
+ Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
+
+void _addBindings(Node node, model, [CustomBindingSyntax syntax]) {
+ if (node is Element) {
+ _addAttributeBindings(node, model, syntax);
+ } else if (node is Text) {
+ _parseAndBind(node, node.text, 'text', model, syntax);
+ }
+
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
+ _addBindings(c, model, syntax);
+ }
+}
+
+
+void _addAttributeBindings(Element element, model, syntax) {
+ element.attributes.forEach((name, value) {
+ if (value == '' && (name == 'bind' || name == 'repeat')) {
+ value = '{{}}';
+ }
+ _parseAndBind(element, value, name, model, syntax);
+ });
+}
+
+void _parseAndBind(Node node, String text, String name, model,
+ CustomBindingSyntax syntax) {
+
+ var tokens = _parseMustacheTokens(text);
+ if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
+ return;
+ }
+
+ 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');
+}
+
+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);
+}
+
+class _BindingToken {
+ final String value;
+ final bool isBinding;
+
+ _BindingToken(this.value, {this.isBinding: false});
+
+ bool get isText => !isBinding;
+}
+
+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;
+}
+
+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;
+ }
+}
+
+void _removeAllBindingsRecursively(Node node) {
+ node.unbindAll();
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
+ _removeAllBindingsRecursively(c);
+ }
+}
+
+void _removeTemplateChild(Node parent, Node child) {
+ child._templateInstance = null;
+ if (child is Element && child.isTemplate) {
+ // Make sure we stop observing when we remove an element.
+ var templateIterator = child._templateIterator;
+ if (templateIterator != null) {
+ templateIterator.abandon();
+ child._templateIterator = null;
+ }
+ }
+ child.remove();
+ _removeAllBindingsRecursively(child);
+}
+
+class _InstanceCursor {
+ final Element _template;
+ Node _terminator;
+ Node _previousTerminator;
+ int _previousIndex = -1;
+ int _index = 0;
+
+ _InstanceCursor(this._template, [index]) {
+ _terminator = _template;
+ if (index != null) {
+ while (index-- > 0) {
+ next();
+ }
+ }
+ }
+
+ void next() {
+ _previousTerminator = _terminator;
+ _previousIndex = _index;
+ _index++;
+
+ while (_index > _terminator._instanceTerminatorCount) {
+ _index -= _terminator._instanceTerminatorCount;
+ _terminator = _terminator.nextNode;
+ if (_terminator is Element && _terminator.tagName == 'TEMPLATE') {
+ _index += _instanceCount(_terminator);
+ }
+ }
+ }
+
+ void abandon() {
+ assert(_instanceCount(_template) > 0);
+ assert(_terminator._instanceTerminatorCount > 0);
+ assert(_index > 0);
+
+ _terminator._instanceTerminatorCount--;
+ _index--;
+ }
+
+ void insert(fragment) {
+ assert(_template.parentNode != null);
+
+ _previousTerminator = _terminator;
+ _previousIndex = _index;
+ _index++;
+
+ _terminator = fragment.$dom_lastChild;
+ if (_terminator == null) _terminator = _previousTerminator;
+ _template.parentNode.insertBefore(fragment, _previousTerminator.nextNode);
+
+ _terminator._instanceTerminatorCount++;
+ if (_terminator != _previousTerminator) {
+ while (_previousTerminator._instanceTerminatorCount >
+ _previousIndex) {
+ _previousTerminator._instanceTerminatorCount--;
+ _terminator._instanceTerminatorCount++;
+ }
+ }
+ }
+
+ void remove() {
+ assert(_previousIndex != -1);
+ assert(_previousTerminator != null &&
+ (_previousIndex > 0 || _previousTerminator == _template));
+ assert(_terminator != null && _index > 0);
+ assert(_template.parentNode != null);
+ assert(_instanceCount(_template) > 0);
+
+ if (_previousTerminator == _terminator) {
+ assert(_index == _previousIndex + 1);
+ _terminator._instanceTerminatorCount--;
+ _terminator = _template;
+ _previousTerminator = null;
+ _previousIndex = -1;
+ return;
+ }
+
+ _terminator._instanceTerminatorCount--;
+
+ var parent = _template.parentNode;
+ while (_previousTerminator.nextNode != _terminator) {
+ _removeTemplateChild(parent, _previousTerminator.nextNode);
+ }
+ _removeTemplateChild(parent, _terminator);
+
+ _terminator = _previousTerminator;
+ _index = _previousIndex;
+ _previousTerminator = null;
+ _previousIndex = -1; // 0?
+ }
+}
+
+
+class _TemplateIterator {
+ final Element _templateElement;
+ int instanceCount = 0;
+ List iteratedValue;
+ bool observing = false;
+ CompoundBinding inputs;
+ _Binding valueBinding;
+ StreamSubscription _sub;
+
+ _TemplateIterator(this._templateElement) {
+ inputs = new CompoundBinding(resolveInputs);
+ valueBinding = new _Binding(inputs, 'value', valueChanged);
+ }
+
+ Object resolveInputs(Map values) {
+ if (values.containsKey('if') && !_templateBooleanConversion(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(handleChange);
+ }
+
+ handleSplices([new ListChangeDelta(0, addedCount: iteratedValue.length)]);
+ }
+
+ // TODO(jmesserly): these properties appear not be finished. I think it's
+ // part of custom syntax like repeat="i in items".
+ getInstanceModel(model, syntax) => model;
+ getInstanceFragment(syntax) => _templateElement.createInstance();
+
+ void handleChange(List<ChangeRecord> records) {
+ handleSplices(summarizeListChanges(iteratedValue, records));
+ }
+
+ void handleSplices(List<ListChangeDelta> splices) {
+ var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
+
+ for (var splice in splices) {
+ for (var removed in splice.removed) {
+ var cursor = new _InstanceCursor(_templateElement, splice.index + 1);
+ cursor.remove();
+ instanceCount--;
+ }
+
+ for (var addIndex = splice.index;
+ addIndex < splice.index + splice.addedCount;
+ addIndex++) {
+
+ var model = getInstanceModel(iteratedValue[addIndex], syntax);
+ var fragment = getInstanceFragment(syntax);
+
+ _addBindings(fragment, model, syntax);
+ _addTemplateInstanceRecord(fragment, model);
+
+ var cursor = new _InstanceCursor(_templateElement, addIndex);
+ cursor.insert(fragment);
+ instanceCount++;
+ }
+ }
+ }
+
+ void unobserve() {
+ if (_sub == null) return;
+ _sub.cancel();
+ _sub = null;
+ }
+
+ void clear() {
+ unobserve();
+
+ iteratedValue = null;
+ if (instanceCount == 0) return;
+
+ for (var i = 0; i < instanceCount; i++) {
+ var cursor = new _InstanceCursor(_templateElement, 1);
+ cursor.remove();
+ }
+
+ instanceCount = 0;
+ }
+
+ void abandon() {
+ unobserve();
+ valueBinding.dispose();
+ inputs.dispose();
+ }
+}
+
+int _instanceCount(Element element) {
+ var templateIterator = element._templateIterator;
+ return templateIterator != null ? templateIterator.instanceCount : 0;
+}

Powered by Google App Engine
This is Rietveld 408576698