| 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 |