OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 part of template_binding; | |
6 | |
7 // This code is a port of what was formerly known as Model-Driven-Views, now | |
8 // located at: | |
9 // https://github.com/polymer/TemplateBinding | |
10 // https://github.com/polymer/NodeBind | |
11 | |
12 // TODO(jmesserly): not sure what kind of boolean conversion rules to | |
13 // apply for template data-binding. HTML attributes are true if they're | |
14 // present. However Dart only treats "true" as true. Since this is HTML we'll | |
15 // use something closer to the HTML rules: null (missing) and false are false, | |
16 // everything else is true. | |
17 // See: https://github.com/polymer/TemplateBinding/issues/59 | |
18 bool _toBoolean(value) => null != value && false != value; | |
19 | |
20 // Dart note: this was added to decouple the MustacheTokens.parse function from | |
21 // the rest of template_binding. | |
22 _getDelegateFactory(name, node, delegate) { | |
23 if (delegate == null) return null; | |
24 return (pathString) => delegate.prepareBinding(pathString, name, node); | |
25 } | |
26 | |
27 _InstanceBindingMap _getBindings(Node node, BindingDelegate delegate) { | |
28 if (node is Element) { | |
29 return _parseAttributeBindings(node, delegate); | |
30 } | |
31 | |
32 if (node is Text) { | |
33 var tokens = MustacheTokens.parse(node.text, | |
34 _getDelegateFactory('text', node, delegate)); | |
35 if (tokens != null) return new _InstanceBindingMap(['text', tokens]); | |
36 } | |
37 | |
38 return null; | |
39 } | |
40 | |
41 void _addBindings(Node node, model, [BindingDelegate delegate]) { | |
42 final bindings = _getBindings(node, delegate); | |
43 if (bindings != null) { | |
44 _processBindings(node, bindings, model); | |
45 } | |
46 | |
47 for (var c = node.firstChild; c != null; c = c.nextNode) { | |
48 _addBindings(c, model, delegate); | |
49 } | |
50 } | |
51 | |
52 MustacheTokens _parseWithDefault(Element element, String name, | |
53 BindingDelegate delegate) { | |
54 | |
55 var v = element.attributes[name]; | |
56 if (v == '') v = '{{}}'; | |
57 return MustacheTokens.parse(v, _getDelegateFactory(name, element, delegate)); | |
58 } | |
59 | |
60 _InstanceBindingMap _parseAttributeBindings(Element element, | |
61 BindingDelegate delegate) { | |
62 | |
63 var bindings = null; | |
64 var ifFound = false; | |
65 var bindFound = false; | |
66 var isTemplateNode = isSemanticTemplate(element); | |
67 | |
68 element.attributes.forEach((name, value) { | |
69 // Allow bindings expressed in attributes to be prefixed with underbars. | |
70 // We do this to allow correct semantics for browsers that don't implement | |
71 // <template> where certain attributes might trigger side-effects -- and | |
72 // for IE which sanitizes certain attributes, disallowing mustache | |
73 // replacements in their text. | |
74 while (name[0] == '_') { | |
75 name = name.substring(1); | |
76 } | |
77 | |
78 if (isTemplateNode && | |
79 (name == 'bind' || name == 'if' || name == 'repeat')) { | |
80 return; | |
81 } | |
82 | |
83 var tokens = MustacheTokens.parse(value, | |
84 _getDelegateFactory(name, element, delegate)); | |
85 if (tokens != null) { | |
86 if (bindings == null) bindings = []; | |
87 bindings..add(name)..add(tokens); | |
88 } | |
89 }); | |
90 | |
91 if (isTemplateNode) { | |
92 if (bindings == null) bindings = []; | |
93 var result = new _TemplateBindingMap(bindings) | |
94 .._if = _parseWithDefault(element, 'if', delegate) | |
95 .._bind = _parseWithDefault(element, 'bind', delegate) | |
96 .._repeat = _parseWithDefault(element, 'repeat', delegate); | |
97 | |
98 // Treat <template if> as <template bind if> | |
99 if (result._if != null && result._bind == null && result._repeat == null) { | |
100 result._bind = MustacheTokens.parse('{{}}', | |
101 _getDelegateFactory('bind', element, delegate)); | |
102 } | |
103 | |
104 return result; | |
105 } | |
106 | |
107 return bindings == null ? null : new _InstanceBindingMap(bindings); | |
108 } | |
109 | |
110 _processOneTimeBinding(String name, MustacheTokens tokens, Node node, model) { | |
111 | |
112 if (tokens.hasOnePath) { | |
113 var delegateFn = tokens.getPrepareBinding(0); | |
114 var value = delegateFn != null ? delegateFn(model, node, true) : | |
115 tokens.getPath(0).getValueFrom(model); | |
116 return tokens.isSimplePath ? value : tokens.combinator(value); | |
117 } | |
118 | |
119 // Tokens uses a striding scheme to essentially store a sequence of structs in | |
120 // the list. See _MustacheTokens for more information. | |
121 var values = new List(tokens.length); | |
122 for (int i = 0; i < tokens.length; i++) { | |
123 Function delegateFn = tokens.getPrepareBinding(i); | |
124 values[i] = delegateFn != null ? | |
125 delegateFn(model, node, false) : | |
126 tokens.getPath(i).getValueFrom(model); | |
127 } | |
128 return tokens.combinator(values); | |
129 } | |
130 | |
131 _processSinglePathBinding(String name, MustacheTokens tokens, Node node, | |
132 model) { | |
133 Function delegateFn = tokens.getPrepareBinding(0); | |
134 var observer = delegateFn != null ? | |
135 delegateFn(model, node, false) : | |
136 new PathObserver(model, tokens.getPath(0)); | |
137 | |
138 return tokens.isSimplePath ? observer : | |
139 new ObserverTransform(observer, tokens.combinator); | |
140 } | |
141 | |
142 _processBinding(String name, MustacheTokens tokens, Node node, model) { | |
143 if (tokens.onlyOneTime) { | |
144 return _processOneTimeBinding(name, tokens, node, model); | |
145 } | |
146 if (tokens.hasOnePath) { | |
147 return _processSinglePathBinding(name, tokens, node, model); | |
148 } | |
149 | |
150 var observer = new CompoundObserver(); | |
151 | |
152 for (int i = 0; i < tokens.length; i++) { | |
153 bool oneTime = tokens.getOneTime(i); | |
154 Function delegateFn = tokens.getPrepareBinding(i); | |
155 | |
156 if (delegateFn != null) { | |
157 var value = delegateFn(model, node, oneTime); | |
158 if (oneTime) { | |
159 observer.addPath(value); | |
160 } else { | |
161 observer.addObserver(value); | |
162 } | |
163 continue; | |
164 } | |
165 | |
166 PropertyPath path = tokens.getPath(i); | |
167 if (oneTime) { | |
168 observer.addPath(path.getValueFrom(model)); | |
169 } else { | |
170 observer.addPath(model, path); | |
171 } | |
172 } | |
173 | |
174 return new ObserverTransform(observer, tokens.combinator); | |
175 } | |
176 | |
177 void _processBindings(Node node, _InstanceBindingMap map, model, | |
178 [List<Bindable> instanceBindings]) { | |
179 | |
180 final bindings = map.bindings; | |
181 final nodeExt = nodeBind(node); | |
182 for (var i = 0; i < bindings.length; i += 2) { | |
183 var name = bindings[i]; | |
184 var tokens = bindings[i + 1]; | |
185 | |
186 var value = _processBinding(name, tokens, node, model); | |
187 var binding = nodeExt.bind(name, value, oneTime: tokens.onlyOneTime); | |
188 if (binding != null && instanceBindings != null) { | |
189 instanceBindings.add(binding); | |
190 } | |
191 } | |
192 | |
193 nodeExt.bindFinished(); | |
194 if (map is! _TemplateBindingMap) return; | |
195 | |
196 final templateExt = nodeBindFallback(node); | |
197 templateExt._model = model; | |
198 | |
199 var iter = templateExt._processBindingDirectives(map); | |
200 if (iter != null && instanceBindings != null) { | |
201 instanceBindings.add(iter); | |
202 } | |
203 } | |
204 | |
205 | |
206 // Note: this doesn't really implement most of Bindable. See: | |
207 // https://github.com/Polymer/TemplateBinding/issues/147 | |
208 class _TemplateIterator extends Bindable { | |
209 final TemplateBindExtension _templateExt; | |
210 | |
211 final List<DocumentFragment> _instances = []; | |
212 | |
213 /** A copy of the last rendered [_presentValue] list state. */ | |
214 final List _iteratedValue = []; | |
215 | |
216 List _presentValue; | |
217 | |
218 bool _closed = false; | |
219 | |
220 // Dart note: instead of storing these in a Map like JS, or using a separate | |
221 // object (extra memory overhead) we just inline the fields. | |
222 var _ifValue, _value; | |
223 | |
224 // TODO(jmesserly): lots of booleans in this object. Bitmask? | |
225 bool _hasIf, _hasRepeat; | |
226 bool _ifOneTime, _oneTime; | |
227 | |
228 StreamSubscription _listSub; | |
229 | |
230 bool _initPrepareFunctions = false; | |
231 PrepareInstanceModelFunction _instanceModelFn; | |
232 PrepareInstancePositionChangedFunction _instancePositionChangedFn; | |
233 | |
234 _TemplateIterator(this._templateExt); | |
235 | |
236 open(callback) => throw new StateError('binding already opened'); | |
237 get value => _value; | |
238 | |
239 Element get _templateElement => _templateExt._node; | |
240 | |
241 void _closeDependencies() { | |
242 if (_ifValue is Bindable) { | |
243 _ifValue.close(); | |
244 _ifValue = null; | |
245 } | |
246 if (_value is Bindable) { | |
247 _value.close(); | |
248 _value = null; | |
249 } | |
250 } | |
251 | |
252 void _updateDependencies(_TemplateBindingMap directives, model) { | |
253 _closeDependencies(); | |
254 | |
255 final template = _templateElement; | |
256 | |
257 _hasIf = directives._if != null; | |
258 _hasRepeat = directives._repeat != null; | |
259 | |
260 var ifValue = true; | |
261 if (_hasIf) { | |
262 _ifOneTime = directives._if.onlyOneTime; | |
263 _ifValue = _processBinding('if', directives._if, template, model); | |
264 ifValue = _ifValue; | |
265 | |
266 // oneTime if & predicate is false. nothing else to do. | |
267 if (_ifOneTime && !_toBoolean(ifValue)) { | |
268 _valueChanged(null); | |
269 return; | |
270 } | |
271 | |
272 if (!_ifOneTime) { | |
273 ifValue = (ifValue as Bindable).open(_updateIfValue); | |
274 } | |
275 } | |
276 | |
277 if (_hasRepeat) { | |
278 _oneTime = directives._repeat.onlyOneTime; | |
279 _value = _processBinding('repeat', directives._repeat, template, model); | |
280 } else { | |
281 _oneTime = directives._bind.onlyOneTime; | |
282 _value = _processBinding('bind', directives._bind, template, model); | |
283 } | |
284 | |
285 var value = _value; | |
286 if (!_oneTime) { | |
287 value = _value.open(_updateIteratedValue); | |
288 } | |
289 | |
290 if (!_toBoolean(ifValue)) { | |
291 _valueChanged(null); | |
292 return; | |
293 } | |
294 | |
295 _updateValue(value); | |
296 } | |
297 | |
298 /// Gets the updated value of the bind/repeat. This can potentially call | |
299 /// user code (if a bindingDelegate is set up) so we try to avoid it if we | |
300 /// already have the value in hand (from Observer.open). | |
301 Object _getUpdatedValue() { | |
302 var value = _value; | |
303 // Dart note: x.discardChanges() is x.value in Dart. | |
304 if (!_toBoolean(_oneTime)) value = value.value; | |
305 return value; | |
306 } | |
307 | |
308 void _updateIfValue(ifValue) { | |
309 if (!_toBoolean(ifValue)) { | |
310 _valueChanged(null); | |
311 return; | |
312 } | |
313 _updateValue(_getUpdatedValue()); | |
314 } | |
315 | |
316 void _updateIteratedValue(value) { | |
317 if (_hasIf) { | |
318 var ifValue = _ifValue; | |
319 if (!_ifOneTime) ifValue = (ifValue as Bindable).value; | |
320 if (!_toBoolean(ifValue)) { | |
321 _valueChanged([]); | |
322 return; | |
323 } | |
324 } | |
325 | |
326 _updateValue(value); | |
327 } | |
328 | |
329 void _updateValue(Object value) { | |
330 if (!_hasRepeat) value = [value]; | |
331 _valueChanged(value); | |
332 } | |
333 | |
334 void _valueChanged(Object value) { | |
335 if (value is! List) { | |
336 if (value is Iterable) { | |
337 // Dart note: we support Iterable by calling toList. | |
338 // But we need to be careful to observe the original iterator if it | |
339 // supports that. | |
340 value = (value as Iterable).toList(); | |
341 } else { | |
342 value = []; | |
343 } | |
344 } | |
345 | |
346 if (identical(value, _iteratedValue)) return; | |
347 | |
348 _unobserve(); | |
349 _presentValue = value; | |
350 | |
351 if (value is ObservableList && _hasRepeat && !_oneTime) { | |
352 // Make sure any pending changes aren't delivered, since we're getting | |
353 // a snapshot at this point in time. | |
354 value.discardListChages(); | |
355 _listSub = value.listChanges.listen(_handleSplices); | |
356 } | |
357 | |
358 _handleSplices(ObservableList.calculateChangeRecords( | |
359 _iteratedValue != null ? _iteratedValue : [], | |
360 _presentValue != null ? _presentValue : [])); | |
361 } | |
362 | |
363 Node _getLastInstanceNode(int index) { | |
364 if (index == -1) return _templateElement; | |
365 // TODO(jmesserly): we could avoid this expando lookup by caching the | |
366 // instance extension instead of the instance. | |
367 var instance = _instanceExtension[_instances[index]]; | |
368 var terminator = instance._terminator; | |
369 if (terminator == null) return _getLastInstanceNode(index - 1); | |
370 | |
371 if (!isSemanticTemplate(terminator) || | |
372 identical(terminator, _templateElement)) { | |
373 return terminator; | |
374 } | |
375 | |
376 var subtemplateIterator = templateBindFallback(terminator)._iterator; | |
377 if (subtemplateIterator == null) return terminator; | |
378 | |
379 return subtemplateIterator._getLastTemplateNode(); | |
380 } | |
381 | |
382 Node _getLastTemplateNode() => _getLastInstanceNode(_instances.length - 1); | |
383 | |
384 void _insertInstanceAt(int index, DocumentFragment fragment) { | |
385 var previousInstanceLast = _getLastInstanceNode(index - 1); | |
386 var parent = _templateElement.parentNode; | |
387 | |
388 _instances.insert(index, fragment); | |
389 parent.insertBefore(fragment, previousInstanceLast.nextNode); | |
390 } | |
391 | |
392 DocumentFragment _extractInstanceAt(int index) { | |
393 var previousInstanceLast = _getLastInstanceNode(index - 1); | |
394 var lastNode = _getLastInstanceNode(index); | |
395 var parent = _templateElement.parentNode; | |
396 var instance = _instances.removeAt(index); | |
397 | |
398 while (lastNode != previousInstanceLast) { | |
399 var node = previousInstanceLast.nextNode; | |
400 if (node == lastNode) lastNode = previousInstanceLast; | |
401 | |
402 instance.append(node..remove()); | |
403 } | |
404 | |
405 return instance; | |
406 } | |
407 | |
408 void _handleSplices(List<ListChangeRecord> splices) { | |
409 if (_closed || splices.isEmpty) return; | |
410 | |
411 final template = _templateElement; | |
412 | |
413 if (template.parentNode == null) { | |
414 close(); | |
415 return; | |
416 } | |
417 | |
418 ObservableList.applyChangeRecords(_iteratedValue, _presentValue, splices); | |
419 | |
420 final delegate = _templateExt.bindingDelegate; | |
421 | |
422 // Dart note: the JavaScript code relies on the distinction between null | |
423 // and undefined to track whether the functions are prepared. We use a bool. | |
424 if (!_initPrepareFunctions) { | |
425 _initPrepareFunctions = true; | |
426 final delegate = _templateExt._self.bindingDelegate; | |
427 if (delegate != null) { | |
428 _instanceModelFn = delegate.prepareInstanceModel(template); | |
429 _instancePositionChangedFn = | |
430 delegate.prepareInstancePositionChanged(template); | |
431 } | |
432 } | |
433 | |
434 // Instance Removals. | |
435 var instanceCache = new HashMap(equals: identical); | |
436 var removeDelta = 0; | |
437 for (var splice in splices) { | |
438 for (var model in splice.removed) { | |
439 var instance = _extractInstanceAt(splice.index + removeDelta); | |
440 if (instance != _emptyInstance) { | |
441 instanceCache[model] = instance; | |
442 } | |
443 } | |
444 | |
445 removeDelta -= splice.addedCount; | |
446 } | |
447 | |
448 for (var splice in splices) { | |
449 for (var addIndex = splice.index; | |
450 addIndex < splice.index + splice.addedCount; | |
451 addIndex++) { | |
452 | |
453 var model = _iteratedValue[addIndex]; | |
454 DocumentFragment instance = instanceCache.remove(model); | |
455 if (instance == null) { | |
456 try { | |
457 if (_instanceModelFn != null) { | |
458 model = _instanceModelFn(model); | |
459 } | |
460 if (model == null) { | |
461 instance = _emptyInstance; | |
462 } else { | |
463 instance = _templateExt.createInstance(model, delegate); | |
464 } | |
465 } catch (e, s) { | |
466 // Dart note: we propagate errors asynchronously here to avoid | |
467 // disrupting the rendering flow. This is different than in the JS | |
468 // implementation but it should probably be fixed there too. Dart | |
469 // hits this case more because non-existing properties in | |
470 // [PropertyPath] are treated as errors, while JS treats them as | |
471 // null/undefined. | |
472 // TODO(sigmund): this should be a synchronous throw when this is | |
473 // called from createInstance, but that requires enough refactoring | |
474 // that it should be done upstream first. See dartbug.com/17789. | |
475 new Completer().completeError(e, s); | |
476 instance = _emptyInstance; | |
477 } | |
478 } | |
479 | |
480 _insertInstanceAt(addIndex, instance); | |
481 } | |
482 } | |
483 | |
484 for (var instance in instanceCache.values) { | |
485 _closeInstanceBindings(instance); | |
486 } | |
487 | |
488 if (_instancePositionChangedFn != null) _reportInstancesMoved(splices); | |
489 } | |
490 | |
491 void _reportInstanceMoved(int index) { | |
492 var instance = _instances[index]; | |
493 if (instance == _emptyInstance) return; | |
494 | |
495 _instancePositionChangedFn(nodeBind(instance).templateInstance, index); | |
496 } | |
497 | |
498 void _reportInstancesMoved(List<ListChangeRecord> splices) { | |
499 var index = 0; | |
500 var offset = 0; | |
501 for (var splice in splices) { | |
502 if (offset != 0) { | |
503 while (index < splice.index) { | |
504 _reportInstanceMoved(index); | |
505 index++; | |
506 } | |
507 } else { | |
508 index = splice.index; | |
509 } | |
510 | |
511 while (index < splice.index + splice.addedCount) { | |
512 _reportInstanceMoved(index); | |
513 index++; | |
514 } | |
515 | |
516 offset += splice.addedCount - splice.removed.length; | |
517 } | |
518 | |
519 if (offset == 0) return; | |
520 | |
521 var length = _instances.length; | |
522 while (index < length) { | |
523 _reportInstanceMoved(index); | |
524 index++; | |
525 } | |
526 } | |
527 | |
528 void _closeInstanceBindings(DocumentFragment instance) { | |
529 var bindings = _instanceExtension[instance]._bindings; | |
530 for (var binding in bindings) binding.close(); | |
531 } | |
532 | |
533 void _unobserve() { | |
534 if (_listSub == null) return; | |
535 _listSub.cancel(); | |
536 _listSub = null; | |
537 } | |
538 | |
539 void close() { | |
540 if (_closed) return; | |
541 | |
542 _unobserve(); | |
543 _instances.forEach(_closeInstanceBindings); | |
544 _instances.clear(); | |
545 _closeDependencies(); | |
546 _templateExt._iterator = null; | |
547 _closed = true; | |
548 } | |
549 } | |
550 | |
551 // Dart note: the JavaScript version just puts an expando on the array. | |
552 class _BoundNodes { | |
553 final List<Node> nodes; | |
554 final List<Bindable> instanceBindings; | |
555 _BoundNodes(this.nodes, this.instanceBindings); | |
556 } | |
OLD | NEW |