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 |