| 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 html; | 5 part of html; |
| 6 | 6 |
| 7 // This code is a port of Model-Driven-Views: | 7 // This code is a port of Model-Driven-Views: |
| 8 // https://github.com/toolkitchen/mdv | 8 // https://github.com/toolkitchen/mdv |
| 9 // The code mostly comes from src/template_element.js | 9 // The code mostly comes from src/template_element.js |
| 10 | 10 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 37 * | 37 * |
| 38 * TemplateElement.syntax['MySyntax'] = new MySyntax(); | 38 * TemplateElement.syntax['MySyntax'] = new MySyntax(); |
| 39 * | 39 * |
| 40 * See <https://github.com/toolkitchen/mdv/blob/master/docs/syntax.md> for more | 40 * See <https://github.com/toolkitchen/mdv/blob/master/docs/syntax.md> for more |
| 41 * information about Custom Syntax. | 41 * information about Custom Syntax. |
| 42 */ | 42 */ |
| 43 // TODO(jmesserly): if this is just one method, a function type would make it | 43 // TODO(jmesserly): if this is just one method, a function type would make it |
| 44 // more Dart-friendly. | 44 // more Dart-friendly. |
| 45 @Experimental | 45 @Experimental |
| 46 abstract class CustomBindingSyntax { | 46 abstract class CustomBindingSyntax { |
| 47 /** |
| 48 * This syntax method allows for a custom interpretation of the contents of |
| 49 * mustaches (`{{` ... `}}`). |
| 50 * |
| 51 * When a template is inserting an instance, it will invoke this method for |
| 52 * each mustache which is encountered. The function is invoked with four |
| 53 * arguments: |
| 54 * |
| 55 * - [model]: The data context for which this instance is being created. |
| 56 * - [path]: The text contents (trimmed of outer whitespace) of the mustache. |
| 57 * - [name]: The context in which the mustache occurs. Within element |
| 58 * attributes, this will be the name of the attribute. Within text, |
| 59 * this will be 'text'. |
| 60 * - [node]: A reference to the node to which this binding will be created. |
| 61 * |
| 62 * If the method wishes to handle binding, it is required to return an object |
| 63 * which has at least a `value` property that can be observed. If it does, |
| 64 * then MDV will call [Node.bind on the node: |
| 65 * |
| 66 * node.bind(name, retval, 'value'); |
| 67 * |
| 68 * If the 'getBinding' does not wish to override the binding, it should return |
| 69 * null. |
| 70 */ |
| 47 // TODO(jmesserly): I had to remove type annotations from "name" and "node" | 71 // TODO(jmesserly): I had to remove type annotations from "name" and "node" |
| 48 // Normally they are String and Node respectively. But sometimes it will pass | 72 // Normally they are String and Node respectively. But sometimes it will pass |
| 49 // (int name, CompoundBinding node). That seems very confusing; we may want | 73 // (int name, CompoundBinding node). That seems very confusing; we may want |
| 50 // to change this API. | 74 // to change this API. |
| 51 getBinding(model, String path, name, node); | 75 getBinding(model, String path, name, node) => null; |
| 76 |
| 77 /** |
| 78 * This syntax method allows a syntax to provide an alterate model than the |
| 79 * one the template would otherwise use when producing an instance. |
| 80 * |
| 81 * When a template is about to create an instance, it will invoke this method |
| 82 * The function is invoked with two arguments: |
| 83 * |
| 84 * - [template]: The template element which is about to create and insert an |
| 85 * instance. |
| 86 * - [model]: The data context for which this instance is being created. |
| 87 * |
| 88 * The template element will always use the return value of `getInstanceModel` |
| 89 * as the model for the new instance. If the syntax does not wish to override |
| 90 * the value, it should simply return the `model` value it was passed. |
| 91 */ |
| 92 getInstanceModel(Element template, model) => model; |
| 93 |
| 94 /** |
| 95 * This syntax method allows a syntax to provide an alterate expansion of |
| 96 * the [template] contents. When the template wants to create an instance, |
| 97 * it will call this method with the template element. |
| 98 * |
| 99 * By default this will call `template.createInstance()`. |
| 100 */ |
| 101 getInstanceFragment(Element template) => template.createInstance(); |
| 52 } | 102 } |
| 53 | 103 |
| 54 /** The callback used in the [CompoundBinding.combinator] field. */ | 104 /** The callback used in the [CompoundBinding.combinator] field. */ |
| 55 @Experimental | 105 @Experimental |
| 56 typedef Object CompoundBindingCombinator(Map objects); | 106 typedef Object CompoundBindingCombinator(Map objects); |
| 57 | 107 |
| 58 /** Information about the instantiated template. */ | 108 /** Information about the instantiated template. */ |
| 59 @Experimental | 109 @Experimental |
| 60 class TemplateInstance { | 110 class TemplateInstance { |
| 61 // TODO(rafaelw): firstNode & lastNode should be read-synchronous | 111 // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
| (...skipping 347 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 409 templateDescendents.forEach(bootstrap); | 459 templateDescendents.forEach(bootstrap); |
| 410 } | 460 } |
| 411 | 461 |
| 412 final String _allTemplatesSelectors = 'template, option[template], ' + | 462 final String _allTemplatesSelectors = 'template, option[template], ' + |
| 413 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); | 463 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
| 414 | 464 |
| 415 void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { | 465 void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
| 416 if (node is Element) { | 466 if (node is Element) { |
| 417 _addAttributeBindings(node, model, syntax); | 467 _addAttributeBindings(node, model, syntax); |
| 418 } else if (node is Text) { | 468 } else if (node is Text) { |
| 419 _parseAndBind(node, node.text, 'text', model, syntax); | 469 _parseAndBind(node, 'text', node.text, model, syntax); |
| 420 } | 470 } |
| 421 | 471 |
| 422 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | 472 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 423 _addBindings(c, model, syntax); | 473 _addBindings(c, model, syntax); |
| 424 } | 474 } |
| 425 } | 475 } |
| 426 | 476 |
| 427 | 477 |
| 428 void _addAttributeBindings(Element element, model, syntax) { | 478 void _addAttributeBindings(Element element, model, syntax) { |
| 429 element.attributes.forEach((name, value) { | 479 element.attributes.forEach((name, value) { |
| 430 if (value == '' && (name == 'bind' || name == 'repeat')) { | 480 if (value == '' && (name == 'bind' || name == 'repeat')) { |
| 431 value = '{{}}'; | 481 value = '{{}}'; |
| 432 } | 482 } |
| 433 _parseAndBind(element, value, name, model, syntax); | 483 _parseAndBind(element, name, value, model, syntax); |
| 434 }); | 484 }); |
| 435 } | 485 } |
| 436 | 486 |
| 437 void _parseAndBind(Node node, String text, String name, model, | 487 void _parseAndBind(Node node, String name, String text, model, |
| 438 CustomBindingSyntax syntax) { | 488 CustomBindingSyntax syntax) { |
| 439 | 489 |
| 440 var tokens = _parseMustacheTokens(text); | 490 var tokens = _parseMustacheTokens(text); |
| 441 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { | 491 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
| 442 return; | 492 return; |
| 443 } | 493 } |
| 444 | 494 |
| 445 if (tokens.length == 1 && tokens[0].isBinding) { | 495 if (tokens.length == 1 && tokens[0].isBinding) { |
| 446 _bindOrDelegate(node, name, model, tokens[0].value, syntax); | 496 _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
| 447 return; | 497 return; |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 562 var templateIterator = child._templateIterator; | 612 var templateIterator = child._templateIterator; |
| 563 if (templateIterator != null) { | 613 if (templateIterator != null) { |
| 564 templateIterator.abandon(); | 614 templateIterator.abandon(); |
| 565 child._templateIterator = null; | 615 child._templateIterator = null; |
| 566 } | 616 } |
| 567 } | 617 } |
| 568 child.remove(); | 618 child.remove(); |
| 569 _removeAllBindingsRecursively(child); | 619 _removeAllBindingsRecursively(child); |
| 570 } | 620 } |
| 571 | 621 |
| 572 class _InstanceCursor { | |
| 573 final Element _template; | |
| 574 Node _terminator; | |
| 575 Node _previousTerminator; | |
| 576 int _previousIndex = -1; | |
| 577 int _index = 0; | |
| 578 | |
| 579 _InstanceCursor(this._template, [index]) { | |
| 580 _terminator = _template; | |
| 581 if (index != null) { | |
| 582 while (index-- > 0) { | |
| 583 next(); | |
| 584 } | |
| 585 } | |
| 586 } | |
| 587 | |
| 588 void next() { | |
| 589 _previousTerminator = _terminator; | |
| 590 _previousIndex = _index; | |
| 591 _index++; | |
| 592 | |
| 593 while (_index > _terminator._instanceTerminatorCount) { | |
| 594 _index -= _terminator._instanceTerminatorCount; | |
| 595 _terminator = _terminator.nextNode; | |
| 596 if (_terminator is Element && _terminator.tagName == 'TEMPLATE') { | |
| 597 _index += _instanceCount(_terminator); | |
| 598 } | |
| 599 } | |
| 600 } | |
| 601 | |
| 602 void abandon() { | |
| 603 assert(_instanceCount(_template) > 0); | |
| 604 assert(_terminator._instanceTerminatorCount > 0); | |
| 605 assert(_index > 0); | |
| 606 | |
| 607 _terminator._instanceTerminatorCount--; | |
| 608 _index--; | |
| 609 } | |
| 610 | |
| 611 void insert(fragment) { | |
| 612 assert(_template.parentNode != null); | |
| 613 | |
| 614 _previousTerminator = _terminator; | |
| 615 _previousIndex = _index; | |
| 616 _index++; | |
| 617 | |
| 618 _terminator = fragment.$dom_lastChild; | |
| 619 if (_terminator == null) _terminator = _previousTerminator; | |
| 620 _template.parentNode.insertBefore(fragment, _previousTerminator.nextNode); | |
| 621 | |
| 622 _terminator._instanceTerminatorCount++; | |
| 623 if (_terminator != _previousTerminator) { | |
| 624 while (_previousTerminator._instanceTerminatorCount > | |
| 625 _previousIndex) { | |
| 626 _previousTerminator._instanceTerminatorCount--; | |
| 627 _terminator._instanceTerminatorCount++; | |
| 628 } | |
| 629 } | |
| 630 } | |
| 631 | |
| 632 void remove() { | |
| 633 assert(_previousIndex != -1); | |
| 634 assert(_previousTerminator != null && | |
| 635 (_previousIndex > 0 || _previousTerminator == _template)); | |
| 636 assert(_terminator != null && _index > 0); | |
| 637 assert(_template.parentNode != null); | |
| 638 assert(_instanceCount(_template) > 0); | |
| 639 | |
| 640 if (_previousTerminator == _terminator) { | |
| 641 assert(_index == _previousIndex + 1); | |
| 642 _terminator._instanceTerminatorCount--; | |
| 643 _terminator = _template; | |
| 644 _previousTerminator = null; | |
| 645 _previousIndex = -1; | |
| 646 return; | |
| 647 } | |
| 648 | |
| 649 _terminator._instanceTerminatorCount--; | |
| 650 | |
| 651 var parent = _template.parentNode; | |
| 652 while (_previousTerminator.nextNode != _terminator) { | |
| 653 _removeTemplateChild(parent, _previousTerminator.nextNode); | |
| 654 } | |
| 655 _removeTemplateChild(parent, _terminator); | |
| 656 | |
| 657 _terminator = _previousTerminator; | |
| 658 _index = _previousIndex; | |
| 659 _previousTerminator = null; | |
| 660 _previousIndex = -1; // 0? | |
| 661 } | |
| 662 } | |
| 663 | |
| 664 | 622 |
| 665 class _TemplateIterator { | 623 class _TemplateIterator { |
| 666 final Element _templateElement; | 624 final Element _templateElement; |
| 667 int instanceCount = 0; | 625 final List<Node> terminators = []; |
| 626 final CompoundBinding inputs; |
| 668 List iteratedValue; | 627 List iteratedValue; |
| 669 bool observing = false; | |
| 670 final CompoundBinding inputs; | |
| 671 | 628 |
| 672 StreamSubscription _sub; | 629 StreamSubscription _sub; |
| 673 StreamSubscription _valueBinding; | 630 StreamSubscription _valueBinding; |
| 674 | 631 |
| 675 _TemplateIterator(this._templateElement) | 632 _TemplateIterator(this._templateElement) |
| 676 : inputs = new CompoundBinding(resolveInputs) { | 633 : inputs = new CompoundBinding(resolveInputs) { |
| 677 | 634 |
| 678 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); | 635 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
| 679 } | 636 } |
| 680 | 637 |
| (...skipping 22 matching lines...) Expand all Loading... |
| 703 if (value is Observable) { | 660 if (value is Observable) { |
| 704 _sub = value.changes.listen(_handleChanges); | 661 _sub = value.changes.listen(_handleChanges); |
| 705 } | 662 } |
| 706 | 663 |
| 707 int len = iteratedValue.length; | 664 int len = iteratedValue.length; |
| 708 if (len > 0) { | 665 if (len > 0) { |
| 709 _handleChanges([new ListChangeRecord(0, addedCount: len)]); | 666 _handleChanges([new ListChangeRecord(0, addedCount: len)]); |
| 710 } | 667 } |
| 711 } | 668 } |
| 712 | 669 |
| 713 // TODO(jmesserly): port MDV v3. | 670 Node getTerminatorAt(int index) { |
| 714 getInstanceModel(model, syntax) => model; | 671 if (index == -1) return _templateElement; |
| 715 getInstanceFragment(syntax) => _templateElement.createInstance(); | 672 var terminator = terminators[index]; |
| 673 if (terminator is! Element) return terminator; |
| 674 |
| 675 var subIterator = terminator._templateIterator; |
| 676 if (subIterator == null) return terminator; |
| 677 |
| 678 return subIterator.getTerminatorAt(subIterator.terminators.length - 1); |
| 679 } |
| 680 |
| 681 void insertInstanceAt(int index, Node fragment) { |
| 682 var previousTerminator = getTerminatorAt(index - 1); |
| 683 var terminator = fragment.$dom_lastChild; |
| 684 if (terminator == null) terminator = previousTerminator; |
| 685 |
| 686 terminators.insert(index, terminator); |
| 687 var parent = _templateElement.parentNode; |
| 688 parent.insertBefore(fragment, previousTerminator.nextNode); |
| 689 } |
| 690 |
| 691 void removeInstanceAt(int index) { |
| 692 var previousTerminator = getTerminatorAt(index - 1); |
| 693 var terminator = getTerminatorAt(index); |
| 694 terminators.removeAt(index); |
| 695 |
| 696 var parent = _templateElement.parentNode; |
| 697 while (terminator != previousTerminator) { |
| 698 var node = terminator; |
| 699 terminator = node.previousNode; |
| 700 _removeTemplateChild(parent, node); |
| 701 } |
| 702 } |
| 703 |
| 704 void removeAllInstances() { |
| 705 if (terminators.length == 0) return; |
| 706 |
| 707 var previousTerminator = _templateElement; |
| 708 var terminator = getTerminatorAt(terminators.length - 1); |
| 709 terminators.length = 0; |
| 710 |
| 711 var parent = _templateElement.parentNode; |
| 712 while (terminator != previousTerminator) { |
| 713 var node = terminator; |
| 714 terminator = node.previousNode; |
| 715 _removeTemplateChild(parent, node); |
| 716 } |
| 717 } |
| 718 |
| 719 void clear() { |
| 720 unobserve(); |
| 721 removeAllInstances(); |
| 722 iteratedValue = null; |
| 723 } |
| 724 |
| 725 getInstanceModel(model, syntax) { |
| 726 if (syntax != null) { |
| 727 return syntax.getInstanceModel(_templateElement, model); |
| 728 } |
| 729 return model; |
| 730 } |
| 731 |
| 732 getInstanceFragment(syntax) { |
| 733 if (syntax != null) { |
| 734 return syntax.getInstanceFragment(_templateElement); |
| 735 } |
| 736 return _templateElement.createInstance(); |
| 737 } |
| 716 | 738 |
| 717 void _handleChanges(List<ListChangeRecord> splices) { | 739 void _handleChanges(List<ListChangeRecord> splices) { |
| 718 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; | 740 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; |
| 719 | 741 |
| 720 for (var splice in splices) { | 742 for (var splice in splices) { |
| 721 if (splice is! ListChangeRecord) continue; | 743 if (splice is! ListChangeRecord) continue; |
| 722 | 744 |
| 723 for (int i = 0; i < splice.removedCount; i++) { | 745 for (int i = 0; i < splice.removedCount; i++) { |
| 724 var cursor = new _InstanceCursor(_templateElement, splice.index + 1); | 746 removeInstanceAt(splice.index); |
| 725 cursor.remove(); | |
| 726 instanceCount--; | |
| 727 } | 747 } |
| 728 | 748 |
| 729 for (var addIndex = splice.index; | 749 for (var addIndex = splice.index; |
| 730 addIndex < splice.index + splice.addedCount; | 750 addIndex < splice.index + splice.addedCount; |
| 731 addIndex++) { | 751 addIndex++) { |
| 732 | 752 |
| 733 var model = getInstanceModel(iteratedValue[addIndex], syntax); | 753 var model = getInstanceModel(iteratedValue[addIndex], syntax); |
| 754 |
| 734 var fragment = getInstanceFragment(syntax); | 755 var fragment = getInstanceFragment(syntax); |
| 735 | 756 |
| 736 _addBindings(fragment, model, syntax); | 757 _addBindings(fragment, model, syntax); |
| 737 _addTemplateInstanceRecord(fragment, model); | 758 _addTemplateInstanceRecord(fragment, model); |
| 738 | 759 |
| 739 var cursor = new _InstanceCursor(_templateElement, addIndex); | 760 insertInstanceAt(addIndex, fragment); |
| 740 cursor.insert(fragment); | |
| 741 instanceCount++; | |
| 742 } | 761 } |
| 743 } | 762 } |
| 744 } | 763 } |
| 745 | 764 |
| 746 void unobserve() { | 765 void unobserve() { |
| 747 if (_sub == null) return; | 766 if (_sub == null) return; |
| 748 _sub.cancel(); | 767 _sub.cancel(); |
| 749 _sub = null; | 768 _sub = null; |
| 750 } | 769 } |
| 751 | 770 |
| 752 void clear() { | |
| 753 unobserve(); | |
| 754 | |
| 755 iteratedValue = null; | |
| 756 if (instanceCount == 0) return; | |
| 757 | |
| 758 for (var i = 0; i < instanceCount; i++) { | |
| 759 var cursor = new _InstanceCursor(_templateElement, 1); | |
| 760 cursor.remove(); | |
| 761 } | |
| 762 | |
| 763 instanceCount = 0; | |
| 764 } | |
| 765 | |
| 766 void abandon() { | 771 void abandon() { |
| 767 unobserve(); | 772 unobserve(); |
| 768 _valueBinding.cancel(); | 773 _valueBinding.cancel(); |
| 769 inputs.dispose(); | 774 inputs.dispose(); |
| 770 } | 775 } |
| 771 } | 776 } |
| 772 | |
| 773 int _instanceCount(Element element) { | |
| 774 var templateIterator = element._templateIterator; | |
| 775 return templateIterator != null ? templateIterator.instanceCount : 0; | |
| 776 } | |
| OLD | NEW |