| Index: pkg/template_binding/lib/src/template.dart
|
| ===================================================================
|
| --- pkg/template_binding/lib/src/template.dart (revision 37373)
|
| +++ pkg/template_binding/lib/src/template.dart (working copy)
|
| @@ -21,6 +21,8 @@
|
|
|
| _InstanceBindingMap _bindingMap;
|
|
|
| + Node _refContent;
|
| +
|
| TemplateBindExtension._(Element node) : super(node);
|
|
|
| Element get _node => super._node;
|
| @@ -28,6 +30,22 @@
|
| TemplateBindExtension get _self => super._node is TemplateBindExtension
|
| ? _node : this;
|
|
|
| + Bindable bind(String name, value, {bool oneTime: false}) {
|
| + if (name != 'ref') return super.bind(name, value, oneTime: oneTime);
|
| +
|
| +
|
| + var ref = oneTime ? value : value.open((ref) {
|
| + _node.attributes['ref'] = ref;
|
| + _refChanged();
|
| + });
|
| +
|
| + _node.attributes['ref'] = ref;
|
| + _refChanged();
|
| + if (oneTime) return null;
|
| +
|
| + return _updateBindings('ref', value);
|
| + }
|
| +
|
| _TemplateIterator _processBindingDirectives(_TemplateBindingMap directives) {
|
| if (_iterator != null) _iterator._closeDependencies();
|
|
|
| @@ -38,16 +56,19 @@
|
| if (_iterator != null) {
|
| _iterator.close();
|
| _iterator = null;
|
| - bindings.remove('iterator');
|
| }
|
| return null;
|
| }
|
|
|
| if (_iterator == null) {
|
| - bindings['iterator'] = _iterator = new _TemplateIterator(this);
|
| + _iterator = new _TemplateIterator(this);
|
| }
|
|
|
| _iterator._updateDependencies(directives, model);
|
| +
|
| + _templateObserver.observe(_node,
|
| + attributes: true, attributeFilter: ['ref']);
|
| +
|
| return _iterator;
|
| }
|
|
|
| @@ -60,38 +81,47 @@
|
| * bindings without walking the tree. This is not normally necesssary, but is
|
| * used internally by the system.
|
| */
|
| - DocumentFragment createInstance([model, BindingDelegate delegate,
|
| - List<Bindable> instanceBindings]) {
|
| + DocumentFragment createInstance([model, BindingDelegate delegate]) {
|
| + if (delegate == null) delegate = _bindingDelegate;
|
| + if (_refContent == null) _refContent = templateBind(_ref).content;
|
|
|
| - final content = templateBind(ref).content;
|
| - // Dart note: we store _bindingMap on the TemplateBindExtension instead of
|
| - // the "content" because we already have an expando for it.
|
| - var map = _bindingMap;
|
| - if (map == null || !identical(map.content, content)) {
|
| - // TODO(rafaelw): Setup a MutationObserver on content to detect
|
| - // when the instanceMap is invalid.
|
| - map = _createInstanceBindingMap(content, delegate);
|
| - map.content = content;
|
| - _bindingMap = map;
|
| - }
|
| + var content = _refContent;
|
| + if (content.firstChild == null) return _emptyInstance;
|
|
|
| + final map = _getInstanceBindingMap(content, delegate);
|
| final staging = _getTemplateStagingDocument();
|
| final instance = _stagingDocument.createDocumentFragment();
|
| - _templateCreator[instance] = _node;
|
|
|
| + final instanceExt = new _InstanceExtension();
|
| + _instanceExtension[instance] = instanceExt
|
| + .._templateCreator = _node
|
| + .._protoContent = content;
|
| +
|
| final instanceRecord = new TemplateInstance(model);
|
| + nodeBindFallback(instance)._templateInstance = instanceRecord;
|
|
|
| var i = 0;
|
| + bool collectTerminator = false;
|
| for (var c = content.firstChild; c != null; c = c.nextNode, i++) {
|
| + // The terminator of the instance is the clone of the last child of the
|
| + // content. If the last child is an active template, it may produce
|
| + // instances as a result of production, so simply collecting the last
|
| + // child of the instance after it has finished producing may be wrong.
|
| + if (c.nextNode == null) collectTerminator = true;
|
| +
|
| final childMap = map != null ? map.getChild(i) : null;
|
| var clone = _cloneAndBindInstance(c, instance, _stagingDocument,
|
| - childMap, model, delegate, instanceBindings);
|
| + childMap, model, delegate, instanceExt._bindings);
|
| +
|
| nodeBindFallback(clone)._templateInstance = instanceRecord;
|
| + if (collectTerminator) instanceExt._terminator = clone;
|
| }
|
|
|
| instanceRecord._firstNode = instance.firstChild;
|
| instanceRecord._lastNode = instance.lastChild;
|
|
|
| + instanceExt._protoContent = null;
|
| + instanceExt._templateCreator = null;
|
| return instance;
|
| }
|
|
|
| @@ -119,7 +149,12 @@
|
| */
|
| BindingDelegate get bindingDelegate => _bindingDelegate;
|
|
|
| +
|
| void set bindingDelegate(BindingDelegate value) {
|
| + if (_bindingDelegate != null) {
|
| + throw new StateError('Template must be cleared before a new '
|
| + 'bindingDelegate can be assigned');
|
| + }
|
| _bindingDelegate = value;
|
|
|
| // Clear cached state based on the binding delegate.
|
| @@ -144,35 +179,40 @@
|
| _processBindings(_node, map, _model);
|
| }
|
|
|
| - /** Gets the template this node refers to. */
|
| - Element get ref {
|
| - _decorate();
|
| + _refChanged() {
|
| + if (_iterator == null || _refContent == templateBind(_ref).content) return;
|
|
|
| - Element result = null;
|
| - var refId = _node.attributes['ref'];
|
| - if (refId != null) {
|
| - var treeScope = _getTreeScope(_node);
|
| - if (treeScope != null) {
|
| - result = treeScope.getElementById(refId);
|
| - }
|
| - if (result == null) {
|
| - var instanceRoot = _getInstanceRoot(_node);
|
| + _refContent = null;
|
| + _iterator._valueChanged(null);
|
| + _iterator._updateIteratedValue(null);
|
| + }
|
|
|
| - // TODO(jmesserly): this won't work if refId is a number
|
| - // Similar to bug: https://github.com/Polymer/ShadowDOM/issues/340
|
| - if (instanceRoot != null) {
|
| - result = instanceRoot.querySelector('#$refId');
|
| - }
|
| - }
|
| + void clear() {
|
| + _model = null;
|
| + _bindingDelegate = null;
|
| + if (bindings != null) {
|
| + var ref = bindings.remove('ref');
|
| + if (ref != null) ref.close();
|
| }
|
| + _refContent = null;
|
| + if (_iterator == null) return;
|
| + _iterator._valueChanged(null);
|
| + _iterator.close();
|
| + _iterator = null;
|
| + }
|
|
|
| - if (result == null) {
|
| - result = _templateInstanceRef;
|
| - if (result == null) return _node;
|
| + /** Gets the template this node refers to. */
|
| + Element get _ref {
|
| + _decorate();
|
| +
|
| + var ref = _searchRefId(_node, _node.attributes['ref']);
|
| + if (ref == null) {
|
| + ref = _templateInstanceRef;
|
| + if (ref == null) return _node;
|
| }
|
|
|
| - var nextRef = templateBind(result).ref;
|
| - return nextRef != null ? nextRef : result;
|
| + var nextRef = templateBindFallback(ref)._ref;
|
| + return nextRef != null ? nextRef : ref;
|
| }
|
|
|
| /**
|
| @@ -201,6 +241,7 @@
|
| if (_templateIsDecorated == true) return false;
|
|
|
| _injectStylesheet();
|
| + _globalBaseUriWorkaround();
|
|
|
| var templateElementExt = this;
|
| _templateIsDecorated = true;
|
| @@ -274,6 +315,8 @@
|
| var doc = _ownerStagingDocument[owner];
|
| if (doc == null) {
|
| doc = owner.implementation.createHtmlDocument('');
|
| + _isStagingDocument[doc] = true;
|
| + _baseUriWorkaround(doc);
|
| _ownerStagingDocument[owner] = doc;
|
| }
|
| _stagingDocument = doc;
|
| @@ -382,34 +425,107 @@
|
| ..text = '$_allTemplatesSelectors { display: none; }';
|
| document.head.append(style);
|
| }
|
| +
|
| + static bool _initBaseUriWorkaround;
|
| +
|
| + static void _globalBaseUriWorkaround() {
|
| + if (_initBaseUriWorkaround == true) return;
|
| + _initBaseUriWorkaround = true;
|
| +
|
| + var t = document.createElement('template');
|
| + if (t is TemplateElement) {
|
| + var d = t.content.ownerDocument;
|
| + if (d.documentElement == null) {
|
| + d.append(d.createElement('html')).append(d.createElement('head'));
|
| + }
|
| + // don't patch this if TemplateBinding.js already has.
|
| + if (d.head.querySelector('base') == null) {
|
| + _baseUriWorkaround(d);
|
| + }
|
| + }
|
| + }
|
| +
|
| + // TODO(rafaelw): Remove when fix for
|
| + // https://codereview.chromium.org/164803002/
|
| + // makes it to Chrome release.
|
| + static void _baseUriWorkaround(HtmlDocument doc) {
|
| + BaseElement base = doc.createElement('base');
|
| + base.href = document.baseUri;
|
| + doc.head.append(base);
|
| + }
|
| +
|
| + static final _templateObserver = new MutationObserver((records, _) {
|
| + for (MutationRecord record in records) {
|
| + templateBindFallback(record.target)._refChanged();
|
| + }
|
| + });
|
| +
|
| }
|
|
|
| -final _templateCreator = new Expando();
|
| +final DocumentFragment _emptyInstance = () {
|
| + var empty = new DocumentFragment();
|
| + _instanceExtension[empty] = new _InstanceExtension();
|
| + return empty;
|
| +}();
|
|
|
| -_getTreeScope(Node node) {
|
| +// TODO(jmesserly): if we merged with wtih TemplateInstance, it seems like it
|
| +// would speed up some operations (e.g. _getInstanceRoot wouldn't need to walk
|
| +// the parent chain).
|
| +class _InstanceExtension {
|
| + final List _bindings = [];
|
| + Node _terminator;
|
| + Element _templateCreator;
|
| + DocumentFragment _protoContent;
|
| +}
|
| +
|
| +// TODO(jmesserly): this is private in JS but public for us because pkg:polymer
|
| +// uses it.
|
| +List getTemplateInstanceBindings(DocumentFragment fragment) {
|
| + var ext = _instanceExtension[fragment];
|
| + return ext != null ? ext._bindings : ext;
|
| +}
|
| +
|
| +/// Gets the root of the current node's parent chain
|
| +_getFragmentRoot(Node node) {
|
| + var p;
|
| + while ((p = node.parentNode) != null) {
|
| + node = p;
|
| + }
|
| + return node;
|
| +}
|
| +
|
| +Node _searchRefId(Node node, String id) {
|
| + if (id == null || id == '') return null;
|
| +
|
| + final selector = '#$id';
|
| while (true) {
|
| - var parent = node.parentNode;
|
| - if (parent != null) {
|
| - node = parent;
|
| - } else {
|
| - var creator = _templateCreator[node];
|
| - if (creator == null) break;
|
| + node = _getFragmentRoot(node);
|
|
|
| - node = creator;
|
| + Node ref = null;
|
| +
|
| + _InstanceExtension instance = _instanceExtension[node];
|
| + if (instance != null && instance._protoContent != null) {
|
| + ref = instance._protoContent.querySelector(selector);
|
| + } else if (_hasGetElementById(node)) {
|
| + ref = (node as dynamic).getElementById(id);
|
| }
|
| - }
|
|
|
| - // Note: JS code tests that getElementById is present. We can't do that
|
| - // easily, so instead check for the types known to implement it.
|
| - if (node is Document || node is ShadowRoot || node is SvgSvgElement) {
|
| - return node;
|
| + if (ref != null) return ref;
|
| +
|
| + if (instance == null) return null;
|
| + node = instance._templateCreator;
|
| + if (node == null) return null;
|
| }
|
| - return null;
|
| }
|
|
|
| _getInstanceRoot(node) {
|
| while (node.parentNode != null) {
|
| node = node.parentNode;
|
| }
|
| - return _templateCreator[node] != null ? node : null;
|
| + _InstanceExtension instance = _instanceExtension[node];
|
| + return instance != null && instance._templateCreator != null ? node : null;
|
| }
|
| +
|
| +final Expando<_InstanceExtension> _instanceExtension = new Expando();
|
| +
|
| +final _isStagingDocument = new Expando();
|
|
|