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 |