| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 part of template_binding; | 5 part of template_binding; |
| 6 | 6 |
| 7 /** Extensions to [Element]s that behave as templates. */ | 7 /** Extensions to [Element]s that behave as templates. */ |
| 8 class TemplateBindExtension extends _ElementExtension { | 8 class TemplateBindExtension extends _ElementExtension { |
| 9 var _model; | 9 var _model; |
| 10 BindingDelegate _bindingDelegate; | 10 BindingDelegate _bindingDelegate; |
| 11 _TemplateIterator _iterator; | 11 _TemplateIterator _iterator; |
| 12 bool _scheduled = false; | 12 bool _setModelScheduled = false; |
| 13 | 13 |
| 14 Element _templateInstanceRef; | 14 Element _templateInstanceRef; |
| 15 | 15 |
| 16 // Note: only used if `this is! TemplateElement` | 16 // Note: only used if `this is! TemplateElement` |
| 17 DocumentFragment _content; | 17 DocumentFragment _content; |
| 18 bool _templateIsDecorated; | 18 bool _templateIsDecorated; |
| 19 | 19 |
| 20 HtmlDocument _stagingDocument; | 20 HtmlDocument _stagingDocument; |
| 21 | 21 |
| 22 var _bindingMap; | 22 _InstanceBindingMap _bindingMap; |
| 23 | 23 |
| 24 TemplateBindExtension._(Element node) : super(node); | 24 TemplateBindExtension._(Element node) : super(node); |
| 25 | 25 |
| 26 Element get _node => super._node; | 26 Element get _node => super._node; |
| 27 | 27 |
| 28 TemplateBindExtension get _self => super._node is TemplateBindExtension | 28 TemplateBindExtension get _self => super._node is TemplateBindExtension |
| 29 ? _node : this; | 29 ? _node : this; |
| 30 | 30 |
| 31 NodeBinding bind(String name, model, [String path]) { | 31 _TemplateIterator _processBindingDirectives(_TemplateBindingMap directives) { |
| 32 path = path != null ? path : ''; | 32 if (_iterator != null) _iterator._closeDependencies(); |
| 33 |
| 34 if (directives._if == null && |
| 35 directives._bind == null && |
| 36 directives._repeat == null) { |
| 37 |
| 38 if (_iterator != null) { |
| 39 _iterator.close(); |
| 40 _iterator = null; |
| 41 bindings.remove('iterator'); |
| 42 } |
| 43 return null; |
| 44 } |
| 33 | 45 |
| 34 if (_iterator == null) { | 46 if (_iterator == null) { |
| 35 // TODO(jmesserly): since there's only one iterator, we could just | 47 bindings['iterator'] = _iterator = new _TemplateIterator(this); |
| 36 // inline it into this object. | |
| 37 _iterator = new _TemplateIterator(this); | |
| 38 } | 48 } |
| 39 | 49 |
| 40 // Dart note: we return _TemplateBinding instead of _iterator. | 50 _iterator._updateDependencies(directives, model); |
| 41 // See comment on _TemplateBinding class. | 51 return _iterator; |
| 42 switch (name) { | |
| 43 case 'bind': | |
| 44 _iterator..hasBind = true | |
| 45 ..bindModel = model | |
| 46 ..bindPath = path; | |
| 47 _scheduleIterator(); | |
| 48 return bindings[name] = new _TemplateBinding(this, name, model, path); | |
| 49 case 'repeat': | |
| 50 _iterator..hasRepeat = true | |
| 51 ..repeatModel = model | |
| 52 ..repeatPath = path; | |
| 53 _scheduleIterator(); | |
| 54 return bindings[name] = new _TemplateBinding(this, name, model, path); | |
| 55 case 'if': | |
| 56 _iterator..hasIf = true | |
| 57 ..ifModel = model | |
| 58 ..ifPath = path; | |
| 59 _scheduleIterator(); | |
| 60 return bindings[name] = new _TemplateBinding(this, name, model, path); | |
| 61 default: | |
| 62 return super.bind(name, model, path); | |
| 63 } | |
| 64 } | |
| 65 | |
| 66 void unbind(String name) { | |
| 67 switch (name) { | |
| 68 case 'bind': | |
| 69 if (_iterator == null) return; | |
| 70 _iterator..hasBind = false | |
| 71 ..bindModel = null | |
| 72 ..bindPath = null; | |
| 73 _scheduleIterator(); | |
| 74 bindings.remove(name); | |
| 75 return; | |
| 76 case 'repeat': | |
| 77 if (_iterator == null) return; | |
| 78 _iterator..hasRepeat = false | |
| 79 ..repeatModel = null | |
| 80 ..repeatPath = null; | |
| 81 _scheduleIterator(); | |
| 82 bindings.remove(name); | |
| 83 return; | |
| 84 case 'if': | |
| 85 if (_iterator == null) return; | |
| 86 _iterator..hasIf = false | |
| 87 ..ifModel = null | |
| 88 ..ifPath = null; | |
| 89 _scheduleIterator(); | |
| 90 bindings.remove(name); | |
| 91 return; | |
| 92 default: | |
| 93 super.unbind(name); | |
| 94 return; | |
| 95 } | |
| 96 } | |
| 97 | |
| 98 void _scheduleIterator() { | |
| 99 if (!_iterator.depsChanging) { | |
| 100 _iterator.depsChanging = true; | |
| 101 scheduleMicrotask(_iterator.resolve); | |
| 102 } | |
| 103 } | 52 } |
| 104 | 53 |
| 105 /** | 54 /** |
| 106 * Creates an instance of the template, using the provided model and optional | 55 * Creates an instance of the template, using the provided [model] and |
| 107 * binding delegate. | 56 * optional binding [delegate]. |
| 57 * |
| 58 * If [instanceBindings] is supplied, each [Bindable] in the returned |
| 59 * instance will be added to the list. This makes it easy to close all of the |
| 60 * bindings without walking the tree. This is not normally necesssary, but is |
| 61 * used internally by the system. |
| 108 */ | 62 */ |
| 109 DocumentFragment createInstance([model, BindingDelegate delegate, | 63 DocumentFragment createInstance([model, BindingDelegate delegate, |
| 110 List<NodeBinding> bound]) { | 64 List<Bindable> instanceBindings]) { |
| 111 var ref = templateBind(this.ref); | 65 |
| 112 var content = ref.content; | 66 final content = templateBind(ref).content; |
| 113 // Dart note: we store _bindingMap on the TemplateBindExtension instead of | 67 // Dart note: we store _bindingMap on the TemplateBindExtension instead of |
| 114 // the "content" because we already have an expando for it. | 68 // the "content" because we already have an expando for it. |
| 115 var map = ref._bindingMap; | 69 var map = _bindingMap; |
| 116 if (map == null) { | 70 if (map == null || !identical(map.content, content)) { |
| 117 // TODO(rafaelw): Setup a MutationObserver on content to detect | 71 // TODO(rafaelw): Setup a MutationObserver on content to detect |
| 118 // when the instanceMap is invalid. | 72 // when the instanceMap is invalid. |
| 119 map = _createInstanceBindingMap(content, delegate); | 73 map = _createInstanceBindingMap(content, delegate); |
| 120 ref._bindingMap = map; | 74 map.content = content; |
| 75 _bindingMap = map; |
| 121 } | 76 } |
| 122 | 77 |
| 123 var staging = _getTemplateStagingDocument(); | 78 final staging = _getTemplateStagingDocument(); |
| 124 var instance = _deepCloneIgnoreTemplateContent(content, staging); | 79 final instance = _stagingDocument.createDocumentFragment(); |
| 80 _templateCreator[instance] = _node; |
| 125 | 81 |
| 126 _addMapBindings(instance, map, model, delegate, bound); | 82 final instanceRecord = new TemplateInstance(model); |
| 127 // TODO(rafaelw): We can do this more lazily, but setting a sentinel | 83 |
| 128 // in the parent of the template element, and creating it when it's | 84 var i = 0; |
| 129 // asked for by walking back to find the iterating template. | 85 for (var c = content.firstChild; c != null; c = c.nextNode, i++) { |
| 130 _addTemplateInstanceRecord(instance, model); | 86 final childMap = map != null ? map.getChild(i) : null; |
| 87 var clone = _cloneAndBindInstance(c, instance, _stagingDocument, |
| 88 childMap, model, delegate, instanceBindings); |
| 89 nodeBindFallback(clone)._templateInstance = instanceRecord; |
| 90 } |
| 91 |
| 92 instanceRecord._firstNode = instance.firstChild; |
| 93 instanceRecord._lastNode = instance.lastChild; |
| 94 |
| 131 return instance; | 95 return instance; |
| 132 } | 96 } |
| 133 | 97 |
| 134 /** The data model which is inherited through the tree. */ | 98 /** The data model which is inherited through the tree. */ |
| 135 get model => _model; | 99 get model => _model; |
| 136 | 100 |
| 137 void set model(value) { | 101 void set model(value) { |
| 138 _model = value; | 102 _model = value; |
| 139 _ensureSetModelScheduled(); | 103 _ensureSetModelScheduled(); |
| 140 } | 104 } |
| 141 | 105 |
| 142 static Node _deepCloneIgnoreTemplateContent(Node node, stagingDocument) { | 106 static Node _deepCloneIgnoreTemplateContent(Node node, stagingDocument) { |
| 143 var clone = stagingDocument.importNode(node, false); | 107 var clone = stagingDocument.importNode(node, false); |
| 144 if (isSemanticTemplate(clone)) return clone; | 108 if (isSemanticTemplate(clone)) return clone; |
| 145 | 109 |
| 146 for (var c = node.firstChild; c != null; c = c.nextNode) { | 110 for (var c = node.firstChild; c != null; c = c.nextNode) { |
| 147 clone.append(_deepCloneIgnoreTemplateContent(c, stagingDocument)); | 111 clone.append(_deepCloneIgnoreTemplateContent(c, stagingDocument)); |
| 148 } | 112 } |
| 149 return clone; | 113 return clone; |
| 150 } | 114 } |
| 151 | 115 |
| 152 /** | 116 /** |
| 153 * The binding delegate which is inherited through the tree. It can be used | 117 * The binding delegate which is inherited through the tree. It can be used |
| 154 * to configure custom syntax for `{{bindings}}` inside this template. | 118 * to configure custom syntax for `{{bindings}}` inside this template. |
| 155 */ | 119 */ |
| 156 BindingDelegate get bindingDelegate => _bindingDelegate; | 120 BindingDelegate get bindingDelegate => _bindingDelegate; |
| 157 | 121 |
| 158 void set bindingDelegate(BindingDelegate value) { | 122 void set bindingDelegate(BindingDelegate value) { |
| 159 _bindingDelegate = value; | 123 _bindingDelegate = value; |
| 160 _ensureSetModelScheduled(); | 124 |
| 125 // Clear cached state based on the binding delegate. |
| 126 _bindingMap = null; |
| 127 if (_iterator != null) { |
| 128 _iterator._initPrepareFunctions = false; |
| 129 _iterator._instanceModelFn = null; |
| 130 _iterator._instancePositionChangedFn = null; |
| 131 } |
| 161 } | 132 } |
| 162 | 133 |
| 163 _ensureSetModelScheduled() { | 134 _ensureSetModelScheduled() { |
| 164 if (_scheduled) return; | 135 if (_setModelScheduled) return; |
| 165 _decorate(); | 136 _decorate(); |
| 166 _scheduled = true; | 137 _setModelScheduled = true; |
| 167 scheduleMicrotask(_setModel); | 138 scheduleMicrotask(_setModel); |
| 168 } | 139 } |
| 169 | 140 |
| 170 void _setModel() { | 141 void _setModel() { |
| 171 _scheduled = false; | 142 _setModelScheduled = false; |
| 172 _addBindings(_node, _model, _bindingDelegate); | 143 var map = _getBindings(_node, _bindingDelegate); |
| 144 _processBindings(_node, map, _model); |
| 173 } | 145 } |
| 174 | 146 |
| 175 /** Gets the template this node refers to. */ | 147 /** Gets the template this node refers to. */ |
| 176 Element get ref { | 148 Element get ref { |
| 177 _decorate(); | 149 _decorate(); |
| 178 | 150 |
| 179 Element result = null; | 151 Element result = null; |
| 180 var refId = _node.attributes['ref']; | 152 var refId = _node.attributes['ref']; |
| 181 if (refId != null) { | 153 if (refId != null) { |
| 182 var treeScope = _getTreeScope(_node); | 154 var treeScope = _getTreeScope(_node); |
| 183 if (treeScope != null) { | 155 if (treeScope != null) { |
| 184 result = treeScope.getElementById(refId); | 156 result = treeScope.getElementById(refId); |
| 185 } | 157 } |
| 158 if (result == null) { |
| 159 var instanceRoot = _getInstanceRoot(_node); |
| 160 |
| 161 // TODO(jmesserly): this won't work if refId is a number |
| 162 // Similar to bug: https://github.com/Polymer/ShadowDOM/issues/340 |
| 163 if (instanceRoot != null) { |
| 164 result = instanceRoot.querySelector('#$refId'); |
| 165 } |
| 166 } |
| 186 } | 167 } |
| 187 | 168 |
| 188 if (result == null) { | 169 if (result == null) { |
| 189 result = _templateInstanceRef; | 170 result = _templateInstanceRef; |
| 190 if (result == null) return _node; | 171 if (result == null) return _node; |
| 191 } | 172 } |
| 192 | 173 |
| 193 var nextRef = templateBind(result).ref; | 174 var nextRef = templateBind(result).ref; |
| 194 return nextRef != null ? nextRef : result; | 175 return nextRef != null ? nextRef : result; |
| 195 } | 176 } |
| (...skipping 20 matching lines...) Expand all Loading... |
| 216 templateBindFallback(template)._decorate(instanceRef); | 197 templateBindFallback(template)._decorate(instanceRef); |
| 217 | 198 |
| 218 bool _decorate([Element instanceRef]) { | 199 bool _decorate([Element instanceRef]) { |
| 219 // == true check because it starts as a null field. | 200 // == true check because it starts as a null field. |
| 220 if (_templateIsDecorated == true) return false; | 201 if (_templateIsDecorated == true) return false; |
| 221 | 202 |
| 222 _injectStylesheet(); | 203 _injectStylesheet(); |
| 223 | 204 |
| 224 var templateElementExt = this; | 205 var templateElementExt = this; |
| 225 _templateIsDecorated = true; | 206 _templateIsDecorated = true; |
| 226 var isNative = _node is TemplateElement; | 207 var isNativeHtmlTemplate = _node is TemplateElement; |
| 227 var bootstrapContents = isNative; | 208 final bootstrapContents = isNativeHtmlTemplate; |
| 228 var liftContents = !isNative; | 209 final liftContents = !isNativeHtmlTemplate; |
| 229 var liftRoot = false; | 210 var liftRoot = false; |
| 230 | 211 |
| 231 if (!isNative && _isAttributeTemplate(_node)) { | 212 if (!isNativeHtmlTemplate) { |
| 232 if (instanceRef != null) { | 213 if (_isAttributeTemplate(_node)) { |
| 233 // TODO(jmesserly): this is just an assert in TemplateBinding. | 214 if (instanceRef != null) { |
| 234 throw new ArgumentError('instanceRef should not be supplied for ' | 215 // Dart note: this is just an assert in JS. |
| 235 'attribute templates.'); | 216 throw new ArgumentError('instanceRef should not be supplied for ' |
| 217 'attribute templates.'); |
| 218 } |
| 219 templateElementExt = templateBind( |
| 220 _extractTemplateFromAttributeTemplate(_node)); |
| 221 templateElementExt._templateIsDecorated = true; |
| 222 isNativeHtmlTemplate = templateElementExt._node is TemplateElement; |
| 223 liftRoot = true; |
| 224 } else if (_isSvgTemplate(_node)) { |
| 225 templateElementExt = templateBind( |
| 226 _extractTemplateFromSvgTemplate(_node)); |
| 227 templateElementExt._templateIsDecorated = true; |
| 228 isNativeHtmlTemplate = templateElementExt._node is TemplateElement; |
| 236 } | 229 } |
| 237 templateElementExt = templateBind( | 230 } |
| 238 _extractTemplateFromAttributeTemplate(_node)); | |
| 239 templateElementExt._templateIsDecorated = true; | |
| 240 isNative = templateElementExt._node is TemplateElement; | |
| 241 liftRoot = true; | |
| 242 } | |
| 243 | 231 |
| 244 if (!isNative) { | 232 if (!isNativeHtmlTemplate) { |
| 245 var doc = _getOrCreateTemplateContentsOwner(templateElementExt._node); | 233 var doc = _getOrCreateTemplateContentsOwner(templateElementExt._node); |
| 246 templateElementExt._content = doc.createDocumentFragment(); | 234 templateElementExt._content = doc.createDocumentFragment(); |
| 247 } | 235 } |
| 248 | 236 |
| 249 if (instanceRef != null) { | 237 if (instanceRef != null) { |
| 250 // template is contained within an instance, its direct content must be | 238 // template is contained within an instance, its direct content must be |
| 251 // empty | 239 // empty |
| 252 templateElementExt._templateInstanceRef = instanceRef; | 240 templateElementExt._templateInstanceRef = instanceRef; |
| 253 } else if (liftContents) { | 241 } else if (liftContents) { |
| 254 _liftNonNativeChildrenIntoContent(templateElementExt, _node, liftRoot); | 242 _liftNonNativeChildrenIntoContent(templateElementExt, _node, liftRoot); |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 319 case 'bind': | 307 case 'bind': |
| 320 case 'ref': | 308 case 'ref': |
| 321 template.attributes[name] = el.attributes.remove(name); | 309 template.attributes[name] = el.attributes.remove(name); |
| 322 break; | 310 break; |
| 323 } | 311 } |
| 324 } | 312 } |
| 325 | 313 |
| 326 return template; | 314 return template; |
| 327 } | 315 } |
| 328 | 316 |
| 317 static Element _extractTemplateFromSvgTemplate(Element el) { |
| 318 var template = el.ownerDocument.createElement('template'); |
| 319 el.parentNode.insertBefore(template, el); |
| 320 template.attributes.addAll(el.attributes); |
| 321 |
| 322 el.attributes.clear(); |
| 323 el.remove(); |
| 324 return template; |
| 325 } |
| 326 |
| 329 static void _liftNonNativeChildrenIntoContent(TemplateBindExtension template, | 327 static void _liftNonNativeChildrenIntoContent(TemplateBindExtension template, |
| 330 Element el, bool useRoot) { | 328 Element el, bool useRoot) { |
| 331 | 329 |
| 332 var content = template.content; | 330 var content = template.content; |
| 333 if (useRoot) { | 331 if (useRoot) { |
| 334 content.append(el); | 332 content.append(el); |
| 335 return; | 333 return; |
| 336 } | 334 } |
| 337 | 335 |
| 338 var child; | 336 var child; |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 379 static void _injectStylesheet() { | 377 static void _injectStylesheet() { |
| 380 if (_initStyles == true) return; | 378 if (_initStyles == true) return; |
| 381 _initStyles = true; | 379 _initStyles = true; |
| 382 | 380 |
| 383 var style = new StyleElement() | 381 var style = new StyleElement() |
| 384 ..text = '$_allTemplatesSelectors { display: none; }'; | 382 ..text = '$_allTemplatesSelectors { display: none; }'; |
| 385 document.head.append(style); | 383 document.head.append(style); |
| 386 } | 384 } |
| 387 } | 385 } |
| 388 | 386 |
| 389 // TODO(jmesserly): https://github.com/polymer/templatebinding uses | 387 final _templateCreator = new Expando(); |
| 390 // TemplateIterator as the binding. This is a nice performance optimization, | |
| 391 // however it means it doesn't share any of the reflective APIs with | |
| 392 // NodeBinding: https://github.com/Polymer/TemplateBinding/issues/147 | |
| 393 class _TemplateBinding implements NodeBinding { | |
| 394 TemplateBindExtension _ext; | |
| 395 Object _model; | |
| 396 final String property; | |
| 397 final String path; | |
| 398 | |
| 399 Node get node => _ext._node; | |
| 400 | |
| 401 get model => _model; | |
| 402 | |
| 403 bool get closed => _ext == null; | |
| 404 | |
| 405 get value => _observer.value; | |
| 406 | |
| 407 set value(newValue) { | |
| 408 _observer.value = newValue; | |
| 409 } | |
| 410 | |
| 411 // No need to cache this since we only have it to support get/set value. | |
| 412 get _observer { | |
| 413 if ((_model is PathObserver || _model is CompoundPathObserver) && | |
| 414 path == 'value') { | |
| 415 return _model; | |
| 416 } | |
| 417 return new PathObserver(_model, path); | |
| 418 } | |
| 419 | |
| 420 _TemplateBinding(this._ext, this.property, this._model, this.path); | |
| 421 | |
| 422 void valueChanged(newValue) {} | |
| 423 | |
| 424 sanitizeBoundValue(value) => value == null ? '' : '$value'; | |
| 425 | |
| 426 void close() { | |
| 427 if (closed) return; | |
| 428 | |
| 429 // TODO(jmesserly): unlike normal NodeBinding.close methods this will remove | |
| 430 // the binding from _node.bindings. Is that okay? | |
| 431 _ext.unbind(property); | |
| 432 | |
| 433 _model = null; | |
| 434 _ext = null; | |
| 435 } | |
| 436 } | |
| 437 | 388 |
| 438 _getTreeScope(Node node) { | 389 _getTreeScope(Node node) { |
| 439 while (node.parentNode != null) { | 390 while (true) { |
| 440 node = node.parentNode; | 391 var parent = node.parentNode; |
| 392 if (parent != null) { |
| 393 node = parent; |
| 394 } else { |
| 395 var creator = _templateCreator[node]; |
| 396 if (creator == null) break; |
| 397 |
| 398 node = creator; |
| 399 } |
| 441 } | 400 } |
| 442 | 401 |
| 443 // Note: JS code tests that getElementById is present. We can't do that | 402 // Note: JS code tests that getElementById is present. We can't do that |
| 444 // easily, so instead check for the types known to implement it. | 403 // easily, so instead check for the types known to implement it. |
| 445 if (node is Document || node is ShadowRoot || node is SvgSvgElement) { | 404 if (node is Document || node is ShadowRoot || node is SvgSvgElement) { |
| 446 return node; | 405 return node; |
| 447 } | 406 } |
| 448 return null; | 407 return null; |
| 449 } | 408 } |
| 409 |
| 410 _getInstanceRoot(node) { |
| 411 while (node.parentNode != null) { |
| 412 node = node.parentNode; |
| 413 } |
| 414 return _templateCreator[node] != null ? node : null; |
| 415 } |
| OLD | NEW |