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 |