From a4f08a798df7525132ec7b0e18c4c6a8091480e8 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 16 Jul 2014 18:06:02 +0200 Subject: [PATCH] feat(scope): component is the new context BREAKING CHANGE: Scope context is set to the component instance that trigged the creation of the scope (previously it was of a PrototypeMap.) Repercussions: 1) You can not inject a scope in a component or in the root context any more. As the Scope context is set to the Component instance, the scope could not be injected any more. Components should implements the "ScopeAware" interface and declare a "scope" setter in order to get a reference to the scope. before: @Component(...) class MyComponent { Watch watch; Scope scope; MyComponent(Dependency myDep, Scope scope) { watch = scope.rootScope.watch("expression", (v, p) => ...); } } after: @Component(...) class MyComponent implements ScopeAware { Watch watch; MyComponent(Dependency myDep) { // It is an error to add a Scope argument to the // ctor and will result in a DI circular dependency error // The scope is never accessible in the class constructor } void set scope(Scope scope) { // This setter gets called to initialize the scope watch = scope.watch("expression", (v, p) => ...); } } or: @Component(...) class MyComponent implements ScopeAware { Scope scope; MyComponent(Dependency myDep) { // It is an error to add a Scope argument to the // ctor and will result in a DI circular dependency error // The scope is never accessible in the class constructor } } 2) The parent component to an NgForm must have a "$name" field to store the form instance. closes #919 closes #917 --- benchmark/web/tree.dart | 19 +- example/pubspec.lock | 8 +- example/pubspec.yaml | 1 - example/web/animation.dart | 14 +- example/web/animation.html | 8 +- example/web/animation/css_demo.dart | 21 +- example/web/animation/repeat_demo.dart | 12 +- example/web/animation/stress_demo.dart | 7 +- example/web/animation/visibility_demo.dart | 7 +- example/web/bouncing_balls.dart | 18 +- example/web/bouncing_balls.html | 61 +- example/web/form.dart | 17 +- example/web/form_controller.dart | 131 -- example/web/form_controller.html | 16 +- example/web/shadow_dom_components.dart | 9 +- example/web/todo.dart | 14 +- example/web/todo.html | 14 +- lib/change_detection/context_locals.dart | 24 + .../dirty_checking_change_detector.dart | 32 +- lib/change_detection/watch_group.dart | 27 +- lib/core/annotation_src.dart | 48 +- lib/core/module.dart | 1 + lib/core/parser/eval_access.dart | 12 +- lib/core/parser/parser.dart | 66 +- lib/core/parser/utils.dart | 11 + lib/core/scope.dart | 56 +- lib/core_dom/directive_injector.dart | 11 +- lib/core_dom/element_binder.dart | 2 +- lib/core_dom/event_handler.dart | 12 +- lib/core_dom/module_internal.dart | 2 +- .../shadow_dom_component_factory.dart | 20 +- .../shadow_dom_component_factory.dart.orig | 168 ++ .../transcluding_component_factory.dart | 5 +- .../transcluding_component_factory.dart.orig | 122 ++ lib/core_dom/view.dart.orig | 119 ++ lib/directive/module.dart | 3 +- lib/directive/ng_class.dart | 24 +- lib/directive/ng_form.dart | 27 +- lib/directive/ng_include.dart | 1 - lib/directive/ng_include.dart.orig | 61 + lib/directive/ng_repeat.dart | 13 +- lib/directive/ng_repeat.dart.orig | 250 +++ lib/directive/ng_switch.dart | 38 +- lib/directive/ng_switch.dart.orig | 156 ++ lib/mock/test_bed.dart | 4 +- lib/routing/ng_view.dart | 25 +- lib/routing/ng_view.dart.orig | 218 ++ test/angular_spec.dart | 1 + .../change_detection/context_locals_spec.dart | 50 + test/change_detection/watch_group_spec.dart | 31 +- test/core/annotation_src_spec.dart | 6 +- test/core/core_directive_spec.dart | 2 - test/core/parser/parser_spec.dart | 26 +- test/core/scope_spec.dart | 37 +- test/core/scope_spec.dart.orig | 1775 +++++++++++++++++ test/core_dom/compiler_spec.dart | 223 ++- test/core_dom/event_handler_spec.dart | 12 +- test/core_dom/event_handler_spec.dart.orig | 105 + .../core_dom/platform_js_based_shim_spec.dart | 10 +- test/directive/ng_if_spec.dart | 47 +- test/directive/ng_include_spec.dart | 49 +- test/directive/ng_model_spec.dart | 3 +- test/directive/ng_repeat_spec.dart | 15 +- test/directive/ng_repeat_spec.dart.orig | 574 ++++++ test/directive/ng_switch_spec.dart | 2 +- test/io/expression_extractor_spec.dart | 8 +- test/io/test_files/main.dart | 2 +- test/io/test_files/main.html | 21 +- test/routing/ng_view_spec.dart | 63 +- test/tools/html_extractor_spec.dart | 28 +- .../web/relative_uris/foo2/relative_foo.dart | 4 +- 71 files changed, 4364 insertions(+), 665 deletions(-) delete mode 100644 example/web/form_controller.dart create mode 100644 lib/change_detection/context_locals.dart create mode 100644 lib/core_dom/shadow_dom_component_factory.dart.orig create mode 100644 lib/core_dom/transcluding_component_factory.dart.orig create mode 100644 lib/core_dom/view.dart.orig create mode 100644 lib/directive/ng_include.dart.orig create mode 100644 lib/directive/ng_repeat.dart.orig create mode 100644 lib/directive/ng_switch.dart.orig create mode 100644 lib/routing/ng_view.dart.orig create mode 100644 test/change_detection/context_locals_spec.dart create mode 100644 test/core/scope_spec.dart.orig create mode 100644 test/core_dom/event_handler_spec.dart.orig create mode 100644 test/directive/ng_repeat_spec.dart.orig diff --git a/benchmark/web/tree.dart b/benchmark/web/tree.dart index 332f94141..bdca3cc2c 100644 --- a/benchmark/web/tree.dart +++ b/benchmark/web/tree.dart @@ -5,17 +5,14 @@ import 'package:angular/application_factory.dart'; import 'package:angular/change_detection/ast_parser.dart'; import 'dart:html'; -import 'dart:math'; import 'dart:js' as js; @Component( selector: 'tree', template: ' {{ctrl.data.value}}' - '' - '' - '', - publishAs: 'ctrl', - useShadowDom: true) + '' + '' + '') class TreeComponent { @NgOneWay('data') var data; @@ -34,9 +31,7 @@ class TranscludingTreeComponent extends TreeComponent {} @Component( selector: 'tree-url', - templateUrl: 'tree-tmpl.html', - publishAs: 'ctrl', - useShadowDom: true) + templateUrl: 'tree-tmpl.html') class TreeUrlComponent { @NgOneWay('data') var data; @@ -209,19 +204,19 @@ class FreeTreeClass { s.text = " $v"; } }); - + scope.watchAST(treeRightNotNullAST, (v, _) { if (v != true) return; s.append(new SpanElement() ..append(new FreeTreeClass(scope, treeRightAST).element())); }); - + scope.watchAST(treeLeftNotNullAST, (v, _) { if (v != true) return; s.append(new SpanElement() ..append(new FreeTreeClass(scope, treeLeftAST).element())); }); - + return elt; } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 8379b928e..e4b588943 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -18,7 +18,7 @@ packages: barback: description: barback source: hosted - version: "0.14.0+3" + version: "0.14.2" browser: description: browser source: hosted @@ -59,6 +59,10 @@ packages: description: perf_api source: hosted version: "0.0.9" + pool: + description: pool + source: hosted + version: "1.0.1" quiver: description: quiver source: hosted @@ -78,7 +82,7 @@ packages: stack_trace: description: stack_trace source: hosted - version: "0.9.3+2" + version: "1.0.2" typed_mock: description: typed_mock source: hosted diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 992598a6b..bcd0043ea 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,7 +5,6 @@ dependencies: path: ../ browser: any unittest: any - quiver: any web_components: any transformers: diff --git a/example/web/animation.dart b/example/web/animation.dart index 15c4fc3a6..01a2fa1f6 100644 --- a/example/web/animation.dart +++ b/example/web/animation.dart @@ -3,7 +3,6 @@ library animation; import 'package:angular/angular.dart'; import 'package:angular/application_factory.dart'; import 'package:angular/animate/module.dart'; -import 'package:quiver/collection.dart'; part 'animation/repeat_demo.dart'; part 'animation/visibility_demo.dart'; @@ -16,16 +15,6 @@ class AnimationDemo { var currentPage = "About"; } -// Temporary workaround, because context needs to extend Map. -@Injectable() -class AnimationDemoHashMap extends DelegatingMap { - final Map _delegate; - AnimationDemoHashMap(AnimationDemo demo) : _delegate = new Map() { - _delegate['demo'] = demo; - } - Map get delegate => _delegate; -} - class AnimationDemoModule extends Module { AnimationDemoModule() { install(new AnimationModule()); @@ -33,12 +22,11 @@ class AnimationDemoModule extends Module { bind(VisibilityDemo); bind(StressDemo); bind(CssDemo); - bind(AnimationDemo); } } main() { applicationFactory() .addModule(new AnimationDemoModule()) - .rootContextType(AnimationDemoHashMap) + .rootContextType(AnimationDemo) .run(); } diff --git a/example/web/animation.html b/example/web/animation.html index 6328ae2c1..5373d98c4 100644 --- a/example/web/animation.html +++ b/example/web/animation.html @@ -6,13 +6,13 @@ -
+

About

diff --git a/example/web/animation/css_demo.dart b/example/web/animation/css_demo.dart index b35e15281..6732832bf 100644 --- a/example/web/animation/css_demo.dart +++ b/example/web/animation/css_demo.dart @@ -4,25 +4,24 @@ part of animation; selector: 'css-demo', template: '''
- - -
BOX
+ 'a': stateA, + 'b': stateB, + 'c': stateC}">BOX
- ''', - publishAs: 'ctrl') + ''') class CssDemo { bool stateA = false; bool stateB = false; diff --git a/example/web/animation/repeat_demo.dart b/example/web/animation/repeat_demo.dart index 69b995b9b..4a1d84b20 100644 --- a/example/web/animation/repeat_demo.dart +++ b/example/web/animation/repeat_demo.dart @@ -5,19 +5,17 @@ part of animation; useShadowDom: false, template: '''
- - + +
    -
  • +
    • -
    • {{inner}}
    • +
    • {{inner}}
- ''', - publishAs: 'ctrl') + ''') class RepeatDemo { var thing = 0; final items = []; diff --git a/example/web/animation/stress_demo.dart b/example/web/animation/stress_demo.dart index e23a3d26d..ff61532b1 100644 --- a/example/web/animation/stress_demo.dart +++ b/example/web/animation/stress_demo.dart @@ -4,14 +4,13 @@ part of animation; selector: 'stress-demo', template: '''
-
-
+
- ''', - publishAs: 'ctrl') + ''') class StressDemo { bool _visible = true; final numbers = [1, 2]; diff --git a/example/web/animation/visibility_demo.dart b/example/web/animation/visibility_demo.dart index 2d1a7c688..1197cf0e2 100644 --- a/example/web/animation/visibility_demo.dart +++ b/example/web/animation/visibility_demo.dart @@ -4,19 +4,18 @@ part of animation; selector: 'visibility-demo', template: '''
- -
+ +

Hello World. ng-if will create and destroy dom elements each time you toggle me.

-
+

Hello World. ng-hide will add and remove the .ng-hide class from me to show and hide this view of text.

''', - publishAs: 'ctrl', useShadowDom: false) class VisibilityDemo { bool visible = false; diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index fc1057462..8316b8869 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -26,15 +26,9 @@ class BallModel { } } - -@Component( - selector: 'bounce-controller', - publishAs: 'ctrl', - templateUrl: 'bouncing_controller.html', - cssUrl: 'bouncing_controller.css' -) -class BounceController { - RootScope rootScope; +@Injectable() +class BounceController implements ScopeAware { + Scope scope; var lastTime = window.performance.now(); var run = false; var fps = 0; @@ -43,9 +37,8 @@ class BounceController { var balls = []; var ballClassName = 'ball'; - BounceController(this.rootScope) { + BounceController() { changeCount(100); - if (run) tick(); } void toggleCSS() { @@ -76,7 +69,7 @@ class BounceController { void timeDigest() { var start = window.performance.now(); digestTime = currentDigestTime; - rootScope.domRead(() { + scope.rootScope.domRead(() { currentDigestTime = window.performance.now() - start; }); } @@ -135,6 +128,7 @@ class MyModule extends Module { main() { applicationFactory() + .rootContextType(BounceController) .addModule(new MyModule()) .run(); } diff --git a/example/web/bouncing_balls.html b/example/web/bouncing_balls.html index def40285a..f90e398fb 100644 --- a/example/web/bouncing_balls.html +++ b/example/web/bouncing_balls.html @@ -2,9 +2,68 @@ Bouncing balls + - +
+
+
+
+ +
+
+
+
+
+ + {{ fps }} fps. ({{ balls.length }} balls) [{{ 1000 / fps }} ms]
+ Digest: {{ digestTime }} ms
+ +1 + +10 + +100 +
+ -1 + -10 + -100 +
+ ▶❙❙
+ Toggle CSS
+ noop
+
diff --git a/example/web/form.dart b/example/web/form.dart index 4d96eb035..d290c1bb0 100644 --- a/example/web/form.dart +++ b/example/web/form.dart @@ -25,15 +25,20 @@ class FormCtrl { '2560x1440', '2560x1600']; - final Scope scope; - final NgForm form; + Scope scope; final List colors = []; final List formattedColors = []; + NgForm myForm; + NgForm colorForm; + NgForm colorsForm; + Map info; + PreviewCtrl preview; - FormCtrl(this.scope, this.form) { + FormCtrl() { newColor(_COLOR_HEX, '#222'); newColor(_COLOR_HEX, '#444'); newColor(_COLOR_HEX, '#000'); + info = new Map(); } List get colorTypes => _COLOR_TYPES; @@ -41,7 +46,7 @@ class FormCtrl { List get resolutions => _RESOLUTIONS; void submit() { - form.reset(); + myForm.reset(); } int getTotalSquares(inputValue) { @@ -110,8 +115,8 @@ class FormCtrl { selector: '[preview-controller]' ) class PreviewCtrl { - PreviewCtrl(Scope scope) { - scope.context['preview'] = this; + PreviewCtrl(FormCtrl form) { + form.preview = this; } List _collection = []; diff --git a/example/web/form_controller.dart b/example/web/form_controller.dart deleted file mode 100644 index 6d747aa9f..000000000 --- a/example/web/form_controller.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:angular/angular.dart'; - -@Component( - selector: '[form-controller]', - templateUrl: 'form_controller.html', - publishAs: 'form_ctrl', - useShadowDom: false) -class FormCtrl { - static const String _COLOR_HEX = "hex"; - static const String _COLOR_HSL = "hsl"; - static const String _COLOR_RGB = "rgb"; - static const String _COLOR_NAME = "name"; - - static const _COLOR_TYPES = const [_COLOR_RGB, _COLOR_HSL, _COLOR_HEX, _COLOR_NAME]; - - static const _RESOLUTIONS = const ['1024x600', - '1280x800', - '1366x768', - '1440x900', - '1600x900', - '1680x1050', - '1920x1080', - '1920x1200', - '2560x1440', - '2560x1600']; - - final Scope scope; - final NgForm form; - final List colors = []; - final List formattedColors = []; - - FormCtrl(this.scope, this.form) { - newColor(_COLOR_HEX, '#222'); - newColor(_COLOR_HEX, '#444'); - newColor(_COLOR_HEX, '#000'); - } - - List get colorTypes => _COLOR_TYPES; - - List get resolutions => _RESOLUTIONS; - - void submit() { - form.reset(); - } - - int getTotalSquaresFromInput() => getTotalSquares(scope.context['squares']); - - int getTotalSquares(inputValue) { - var value = 4; - if(inputValue != null) { - try { - value = double.parse(inputValue.toString()); - } catch(e) { - } - } - return (value * value).toInt(); - } - - List formatColors() { - formattedColors.clear(); - colors.forEach((color) { - var value = null; - switch(color['type']) { - case _COLOR_HEX: - value = color['hex']; - break; - case _COLOR_HSL: - var hue = color['hue']; - var saturation = color['saturation']; - var luminance = color['luminance']; - if(hue != null && saturation != null && luminance != null) { - value = "hsl($hue, $saturation%, $luminance%)"; - } - break; - case _COLOR_RGB: - var red = color['red']; - var blue = color['blue']; - var green = color['green']; - if(red != null && green != null && blue != null) { - value = "rgb($red, $green, $blue)"; - } - break; - default: //COLOR_NAME - value = color['name']; - break; - } - if(value != null) { - formattedColors.add(value); - } - }); - return formattedColors; - } - - void newColor([String type = _COLOR_HEX, String color]) { - colors.add({ - 'id' : colors.length, - 'type' : type, - 'hex' : type == _COLOR_HEX ? color : '', - 'hue' : '', - 'saturation' : '', - 'luminance' : '', - 'red' : '', - 'green' : '', - 'blue': '', - 'name': '' - }); - } -} - -@Decorator( - selector: '[preview-controller]' -) -class PreviewCtrl { - PreviewCtrl(Scope scope) { - scope.context['preview'] = this; - } - - List _collection = []; - - List expandList(items, limit) { - _collection.clear(); - if(items != null && items.length > 0) { - for (var i = 0; i < limit; i++) { - var x = i % items.length; - _collection.add(items[x]); - } - } - return _collection; - } -} - diff --git a/example/web/form_controller.html b/example/web/form_controller.html index ce5eb0fbd..5f99d60d4 100644 --- a/example/web/form_controller.html +++ b/example/web/form_controller.html @@ -1,8 +1,8 @@
+ ng-submit="submit()" + ng-class="{submitted:myForm.submitted}">
@@ -10,7 +10,7 @@

Wallpaper Resolution Size

@@ -46,13 +46,13 @@

Account Details

Colors

- New Color + New Color -
+
@@ -89,10 +89,10 @@

Colors

-
+
+ ng-repeat="color in preview.expandList(formatColors(), getTotalSquares(info.squares))">
diff --git a/example/web/shadow_dom_components.dart b/example/web/shadow_dom_components.dart index 0f840f87a..16b016f16 100644 --- a/example/web/shadow_dom_components.dart +++ b/example/web/shadow_dom_components.dart @@ -12,16 +12,15 @@ main() { @Component( selector: "my-component", - publishAs: "ctrl", template: """ -
+
Shadow [ ] - + Toggle - off - on + off + on
""", cssUrl: "/css/shadow_dom_components.css") diff --git a/example/web/todo.dart b/example/web/todo.dart index 4135dfa51..60d443a72 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -92,19 +92,9 @@ class Todo { } -// Temporary workaround, because context needs to extend Map. -@Injectable() -class TodoHashMap extends DelegatingMap { - final Map _delegate; - TodoHashMap(Todo todo) : _delegate = new Map() { - _delegate['todo'] = todo; - } - Map get delegate => _delegate; -} - main() { print(window.location.search); - var module = new Module()..bind(PlaybackHttpBackendConfig)..bind(Todo); + var module = new Module()..bind(PlaybackHttpBackendConfig); // If these is a query in the URL, use the server-backed // TodoController. Otherwise, use the stored-data controller. @@ -124,7 +114,7 @@ main() { } applicationFactory() - .rootContextType(TodoHashMap) .addModule(module) + .rootContextType(Todo) .run(); } diff --git a/example/web/todo.html b/example/web/todo.html index abb0f7474..2dcb69f4a 100644 --- a/example/web/todo.html +++ b/example/web/todo.html @@ -15,15 +15,15 @@

Things To Do ;-)

- - + +
-

Remaining {{todo.remaining()}} of {{todo.items.length}} items.

+

Remaining {{ remaining() }} of {{ items.length }} items.

    -
  • +
  • @@ -31,9 +31,9 @@

    Things To Do ;-)

- - - + + +
diff --git a/lib/change_detection/context_locals.dart b/lib/change_detection/context_locals.dart new file mode 100644 index 000000000..05098d68a --- /dev/null +++ b/lib/change_detection/context_locals.dart @@ -0,0 +1,24 @@ +part of angular.watch_group; + +class ContextLocals { + final Map _locals = {}; + + final Object _parentContext; + Object get parentContext => _parentContext; + + ContextLocals(this._parentContext, [Map locals = null]) { + assert(_parentContext != null); + if (locals != null) _locals.addAll(locals); + } + + static ContextLocals wrapper(context, Map locals) => + new ContextLocals(context, locals); + + bool hasProperty(String prop) => _locals.containsKey(prop); + + void operator[]=(String prop, value) { + _locals[prop] = value; + } + + dynamic operator[](String prop) => _locals[prop]; +} diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 0fec28340..6f5423ac5 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -2,6 +2,7 @@ library dirty_checking_change_detector; import 'dart:collection'; import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/watch_group.dart'; /** * [DirtyCheckingChangeDetector] determines which object properties have changed @@ -369,7 +370,7 @@ class _ChangeIterator implements Iterator>{ * removing efficient. [DirtyCheckingRecord] also has a [nextChange] field which * creates a single linked list of all of the changes for efficient traversal. */ -class DirtyCheckingRecord implements Record, WatchRecord { +class DirtyCheckingRecord implements WatchRecord { static const List _MODE_NAMES = const [ 'MARKER', 'NOOP', @@ -423,9 +424,21 @@ class DirtyCheckingRecord implements Record, WatchRecord { * [DirtyCheckingRecord] into different access modes. If Object it sets up * reflection. If [Map] then it sets up map accessor. */ - void set object(obj) { - _object = obj; - if (obj == null) { + void set object(Object object) { + // The watched object can change inside this method, so use _object throughout. + _object = object; + + while (_object is ContextLocals) { + var ctx = _object as ContextLocals; + if (ctx.hasProperty(field)) { + _mode = _MODE_MAP_FIELD_; + _getter = null; + return; + } + _object = ctx.parentContext; + } + + if (_object == null) { _mode = _MODE_IDENTITY_; _getter = null; return; @@ -433,7 +446,8 @@ class DirtyCheckingRecord implements Record, WatchRecord { if (field == null) { _getter = null; - if (obj is Map) { + + if (_object is Map) { if (_mode != _MODE_MAP_) { _mode = _MODE_MAP_; currentValue = new _MapChangeRecord(); @@ -445,8 +459,7 @@ class DirtyCheckingRecord implements Record, WatchRecord { // new reference. currentValue._revertToPreviousState(); } - - } else if (obj is Iterable) { + } else if (_object is Iterable) { if (_mode != _MODE_ITERABLE_) { _mode = _MODE_ITERABLE_; currentValue = new _CollectionChangeRecord(); @@ -465,12 +478,13 @@ class DirtyCheckingRecord implements Record, WatchRecord { return; } - if (obj is Map) { + if (_object is Map) { _mode = _MODE_MAP_FIELD_; _getter = null; } else { + _mode = _MODE_GETTER_OR_METHOD_CLOSURE_; - _getter = _fieldGetterFactory.getter(obj, field); + _getter = _fieldGetterFactory.getter(_object, field); } } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 456541432..be8c4d702 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -7,6 +7,7 @@ import 'package:angular/ng_tracing.dart'; part 'linked_list.dart'; part 'ast.dart'; part 'prototype_map.dart'; +part 'context_locals.dart'; /** * A function that is notified of changes to the model. @@ -87,6 +88,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { /// STATS: Number of invocation watchers (closures/methods) which are in use. int get evalCost => _evalCost; + /// STATS: Number of invocation watchers which are in use including child [WatchGroup]s. int get totalEvalCost { var cost = _evalCost; @@ -784,24 +786,33 @@ class _EvalWatchRecord implements WatchRecord<_Handler> { get object => _object; - set object(value) { + void set object(object) { assert(mode != _MODE_DELETED_); assert(mode != _MODE_MARKER_); assert(mode != _MODE_FUNCTION_); assert(mode != _MODE_PURE_FUNCTION_); assert(mode != _MODE_PURE_FUNCTION_APPLY_); - _object = value; - if (value == null) { + // The watched object can change inside this method, so use _object throughout. + _object = object; + + if (_object == null) { mode = _MODE_NULL_; + } else if (_object is Map) { + mode = _MODE_MAP_CLOSURE_; } else { - if (value is Map) { - mode = _MODE_MAP_CLOSURE_; - } else { - mode = _MODE_FIELD_OR_METHOD_CLOSURE_; - fn = _fieldGetterFactory.getter(value, name); + while (_object is ContextLocals) { + var ctx = _object as ContextLocals; + if (ctx.hasProperty(name)) { + mode = _MODE_MAP_CLOSURE_; + return; + } + _object = ctx.parentContext; } + mode = _MODE_FIELD_OR_METHOD_CLOSURE_; + fn = _fieldGetterFactory.getter(_object, name); } + } bool check() { diff --git a/lib/core/annotation_src.dart b/lib/core/annotation_src.dart index 9aca03cdf..3982aacb0 100644 --- a/lib/core/annotation_src.dart +++ b/lib/core/annotation_src.dart @@ -249,6 +249,39 @@ abstract class Directive { * [ShadowRoot](https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart-dom-html.ShadowRoot) is loaded. */ class Component extends Directive { + /** + * This property is left here for backward compatibility, but it is not required. + * + * Before: + * + * @Component(publishAs: 'ctrl', ...) + * class MyComponent { + * // ... + * } + * + * in component template: {{ctrl.foo}} + * + * After: + * + * @Component(publishAs: 'ctrl', ...) + * class MyComponent { + * // You must add a getter named after the publishAs configuration + * MyComponent get ctrl => this; + * + * // ... + * } + * + * Finally: + * + * @Component() + * class MyComponent {} + * + * in component template: {{foo}} + */ + @Deprecated('next release. This property is left for backward compatibility but setting it has no' + ' effect.') + final String publishAs; + /** * Inlined HTML template for the component. */ @@ -265,13 +298,6 @@ class Component extends Directive { */ final _cssUrls; - /** - * An expression under which the component's controller instance will be - * published into. This allows the expressions in the template to be referring - * to controller instance and its properties. - */ - final String publishAs; - /** * If set to true, this component will always use shadow DOM. * If set to false, this component will never use shadow DOM. @@ -288,7 +314,6 @@ class Component extends Directive { this.template, this.templateUrl, cssUrl, - this.publishAs, DirectiveBinderFn module, map, selector, @@ -297,7 +322,8 @@ class Component extends Directive { exportExpressionAttrs, this.useShadowDom, this.useNgBaseCss: true, - updateBoundElementPropertiesOnEvents + updateBoundElementPropertiesOnEvents, + this.publishAs }) : _cssUrls = cssUrl, super(selector: selector, children: Directive.COMPILE_CHILDREN, @@ -317,7 +343,6 @@ class Component extends Directive { template: template, templateUrl: templateUrl, cssUrl: cssUrls, - publishAs: publishAs, map: newMap, module: module, selector: selector, @@ -326,7 +351,8 @@ class Component extends Directive { exportExpressionAttrs: exportExpressionAttrs, useShadowDom: useShadowDom, useNgBaseCss: useNgBaseCss, - updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents); + updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents, + publishAs: publishAs); } /** diff --git a/lib/core/module.dart b/lib/core/module.dart index bbb35287d..2a2285c16 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -75,6 +75,7 @@ export "package:angular/core/module_internal.dart" show WebPlatform, PrototypeMap, RootScope, + ContextLocals, Scope, ScopeAware, ScopeDigestTTL, diff --git a/lib/core/parser/eval_access.dart b/lib/core/parser/eval_access.dart index 566a69834..966fcf3e1 100644 --- a/lib/core/parser/eval_access.dart +++ b/lib/core/parser/eval_access.dart @@ -38,22 +38,26 @@ class AccessKeyed extends syntax.AccessKeyed { * where we have a pair of pre-compiled getter and setter functions that we * use to do the access the field. */ +// todo(vicb) - parser should not depend on ContextLocals +// todo(vicb) - Map should not be a special case so that we can access the props abstract class AccessFast { String get name; Getter get getter; Setter get setter; - _eval(holder) { + dynamic _eval(holder) { if (holder == null) return null; - return (holder is Map) ? holder[name] : getter(holder); + if (holder is Map) return holder[name]; + return getter(holder); } - _assign(scope, holder, value) { + dynamic _assign(scope, holder, value) { if (holder == null) { _assignToNonExisting(scope, value); return value; } else { - return (holder is Map) ? (holder[name] = value) : setter(holder, value); + if (holder is Map) return holder[name] = value; + return setter(holder, value); } } diff --git a/lib/core/parser/parser.dart b/lib/core/parser/parser.dart index 3b32b3dbe..e86a503cd 100644 --- a/lib/core/parser/parser.dart +++ b/lib/core/parser/parser.dart @@ -7,7 +7,11 @@ import 'package:angular/core/parser/eval.dart'; import 'package:angular/core/parser/utils.dart' show EvalError; import 'package:angular/cache/module.dart'; import 'package:angular/core/annotation_src.dart' hide Formatter; -import 'package:angular/core/module_internal.dart' show FormatterMap; +import 'package:angular/core/module_internal.dart' show + FormatterMap, + ContextLocals; + +import 'package:angular/core/parser/parser.dart'; import 'package:angular/core/parser/lexer.dart'; import 'package:angular/core/parser/parse_expression.dart'; import 'package:angular/utils.dart'; @@ -128,7 +132,7 @@ class _UnwrapExceptionDecorator extends Expression { @Injectable() class RuntimeParserBackend extends ParserBackend { final ClosureMap _closures; - RuntimeParserBackend(this._closures); + RuntimeParserBackend(ClosureMap _closures): _closures = new ClosureMapLocalsAware(_closures); bool isAssignable(Expression expression) => expression.isAssignable; @@ -197,3 +201,61 @@ class RuntimeParserBackend extends ParserBackend { } } +// todo(vicb) Would probably be better to remove this from the parser +class ClosureMapLocalsAware implements ClosureMap { + final ClosureMap wrappedClsMap; + + ClosureMapLocalsAware(this.wrappedClsMap); + + Getter lookupGetter(String name) { + return (o) { + while (o is ContextLocals) { + var ctx = o as ContextLocals; + if (ctx.hasProperty(name)) return ctx[name]; + o = ctx.parentContext; + } + var getter = wrappedClsMap.lookupGetter(name); + return getter(o); + }; + } + + Setter lookupSetter(String name) { + return (o, value) { + while (o is ContextLocals) { + var ctx = o as ContextLocals; + if (ctx.hasProperty(name)) return ctx[name] = value; + o = ctx.parentContext; + } + var setter = wrappedClsMap.lookupSetter(name); + return setter(o, value); + }; + } + + MethodClosure lookupFunction(String name, CallArguments arguments) { + return (o, pArgs, nArgs) { + while (o is ContextLocals) { + var ctx = o as ContextLocals; + if (ctx.hasProperty(name)) { + var fn = ctx[name]; + if (fn is Function) { + var snArgs = {}; + nArgs.forEach((name, value) { + var symbol = wrappedClsMap.lookupGetter(name); + snArgs[symbol] = value; + }); + return Function.apply(fn, pArgs, snArgs); + } else { + throw "Property '$name' is not of type function."; + } + } + o = ctx.parentContext; + } + var fn = wrappedClsMap.lookupFunction(name, arguments); + return fn(o, pArgs, nArgs); + }; + } + + Symbol lookupSymbol(String name) => wrappedClsMap.lookupSymbol(name); +} + + diff --git a/lib/core/parser/utils.dart b/lib/core/parser/utils.dart index 0da2df0e1..cf3c9a42d 100644 --- a/lib/core/parser/utils.dart +++ b/lib/core/parser/utils.dart @@ -2,6 +2,7 @@ library angular.core.parser.utils; import 'package:angular/core/parser/syntax.dart' show Expression; import 'package:angular/core/formatter.dart' show FormatterMap; +import 'package:angular/core/module_internal.dart' show ContextLocals; export 'package:angular/utils.dart' show relaxFnApply, relaxFnArgs, toBool; /// Marker for an uninitialized value. @@ -80,6 +81,11 @@ getKeyed(object, key) { } else if (object == null) { throw new EvalError('Accessing null object'); } else { + while (object is ContextLocals) { + var ctx = object as ContextLocals; + if (ctx.hasProperty(key)) break; + object = ctx.parentContext; + } return object[key]; } } @@ -93,6 +99,11 @@ setKeyed(object, key, value) { } else if (object is Map) { object["$key"] = value; // toString dangerous? } else { + while (object is ContextLocals) { + var ctx = object as ContextLocals; + if (ctx.hasProperty(key)) break; + object = ctx.parentContext; + } object[key] = value; } return value; diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 5e8d10dcb..f2b3d521d 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -69,46 +69,6 @@ class ScopeDigestTTL { ScopeDigestTTL.value(this.ttl); } -//TODO(misko): I don't think this should be in scope. -class ScopeLocals implements Map { - static wrapper(scope, Map locals) => - new ScopeLocals(scope, locals); - - Map _scope; - Map _locals; - - ScopeLocals(this._scope, this._locals); - - void operator []=(String name, value) { - _scope[name] = value; - } - dynamic operator [](String name) { - // Map needed to clear Dart2js warning - Map map = _locals.containsKey(name) ? _locals : _scope; - return map[name]; - } - - bool get isEmpty => _scope.isEmpty && _locals.isEmpty; - bool get isNotEmpty => _scope.isNotEmpty || _locals.isNotEmpty; - List get keys => _scope.keys; - List get values => _scope.values; - int get length => _scope.length; - - void forEach(fn) { - _scope.forEach(fn); - } - dynamic remove(key) => _scope.remove(key); - void clear() { - _scope.clear; - } - bool containsKey(key) => _scope.containsKey(key); - bool containsValue(key) => _scope.containsValue(key); - void addAll(map) { - _scope.addAll(map); - } - dynamic putIfAbsent(key, fn) => _scope.putIfAbsent(key, fn); -} - /** * When a [Directive] or the root context class implements [ScopeAware] the scope * setter will be called to set the [Scope] on this component. @@ -151,7 +111,7 @@ class Scope { int _childScopeNextId = 0; /// The default execution context for [watch]es [observe]ers, and [eval]uation. - final context; + final dynamic context; /// The [RootScope] of the application. final RootScope rootScope; @@ -184,8 +144,8 @@ class Scope { // TODO(misko): WatchGroup should be private. // Instead we should expose performance stats about the watches // such as # of watches, checks/1ms, field checks, function checks, etc - final WatchGroup _readWriteGroup; - final WatchGroup _readOnlyGroup; + WatchGroup _readWriteGroup; + WatchGroup _readOnlyGroup; Scope _childHead, _childTail, _next, _prev; _Streams _streams; @@ -297,8 +257,8 @@ class Scope { expression is String || expression is Function); if (expression is String && expression.isNotEmpty) { - var obj = locals == null ? context : new ScopeLocals(context, locals); - return rootScope._parser(expression).eval(obj); + var ctx = locals == null ? context : new ContextLocals(context, locals); + return rootScope._parser(expression).eval(ctx); } assert(locals == null); @@ -369,8 +329,8 @@ class Scope { var child = new Scope(childContext, rootScope, this, _readWriteGroup.newGroup(childContext), _readOnlyGroup.newGroup(childContext), - '$id:${_childScopeNextId++}', - _stats); + '$id:${_childScopeNextId++}', + _stats); var prev = _childTail; child._prev = prev; @@ -382,7 +342,7 @@ class Scope { /// Creates a child [Scope] that is prototypal with respect to current scope. Scope createProtoChild() { - return createChild(new PrototypeMap(context)); + return createChild(new ContextLocals(context)); } /** diff --git a/lib/core_dom/directive_injector.dart b/lib/core_dom/directive_injector.dart index 2bc7475e6..7b2231cf2 100644 --- a/lib/core_dom/directive_injector.dart +++ b/lib/core_dom/directive_injector.dart @@ -444,10 +444,12 @@ class ComponentDirectiveInjector extends DirectiveInjector { final TemplateLoader _templateLoader; final ShadowRoot _shadowRoot; + // The key for the directive that triggered the creation of this injector. + final Key _typeKey; ComponentDirectiveInjector(DirectiveInjector parent, Injector appInjector, EventHandler eventHandler, Scope scope, - this._templateLoader, this._shadowRoot, LightDom lightDom, + this._templateLoader, this._shadowRoot, LightDom lightDom, this._typeKey, [View view, ShadowBoundary shadowBoundary]) : super(parent, appInjector, parent._node, parent._nodeAttrs, eventHandler, scope, parent._animate, view, shadowBoundary) { @@ -463,6 +465,13 @@ class ComponentDirectiveInjector extends DirectiveInjector { case SHADOW_ROOT_KEY_ID: return _shadowRoot; case DIRECTIVE_INJECTOR_KEY_ID: return _parent; case COMPONENT_DIRECTIVE_INJECTOR_KEY_ID: return this; + // Currently, this is guaranteed to be called after controller creation. + case SCOPE_KEY_ID: + if (scope == null) { + Scope parentScope = _parent.scope; + scope = parentScope.createChild(getByKey(_typeKey)); + } + return scope; default: return super._getById(keyId); } } diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index ec24f1bfa..b77c5d35b 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -120,7 +120,7 @@ class ElementBinder { } void _bindCallback(dstPathFn, controller, expression, scope) { - dstPathFn.assign(controller, _parser(expression).bind(scope.context, ScopeLocals.wrapper)); + dstPathFn.assign(controller, _parser(expression).bind(scope.context, ContextLocals.wrapper)); } diff --git a/lib/core_dom/event_handler.dart b/lib/core_dom/event_handler.dart index 629daecec..8f2bc2560 100644 --- a/lib/core_dom/event_handler.dart +++ b/lib/core_dom/event_handler.dart @@ -4,7 +4,7 @@ typedef void EventFunction(event); /** * [EventHandler] is responsible for handling events bound using on-* syntax - * (i.e. `on-click="ctrl.doSomething();"`). The root of the application has an + * (i.e. `on-click="doSomething();"`). The root of the application has an * EventHandler attached as does every [Component]. * * Events bound within [Component] are handled by EventHandler attached to @@ -16,12 +16,14 @@ typedef void EventFunction(event); * Example: * *
- * ; + * ; *
* - * @Component(selector: '[foo]', publishAs: ctrl) - * class FooController { - * say(String something) => print(something); + * @Component(selector: '[foo]') + * class FooComponent { + * void say(String something) { + * print(something); + * } * } * * When button is clicked, "Hello" will be printed in the console. diff --git a/lib/core_dom/module_internal.dart b/lib/core_dom/module_internal.dart index 25fbb667a..ebfb3ab81 100644 --- a/lib/core_dom/module_internal.dart +++ b/lib/core_dom/module_internal.dart @@ -26,7 +26,7 @@ import 'package:angular/core_dom/resource_url_resolver.dart'; export 'package:angular/core_dom/resource_url_resolver.dart' show ResourceUrlResolver, ResourceResolverConfig; -import 'package:angular/change_detection/watch_group.dart' show Watch, PrototypeMap; +import 'package:angular/change_detection/watch_group.dart' show Watch, ContextLocals; import 'package:angular/change_detection/ast_parser.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/ng_tracing.dart'; diff --git a/lib/core_dom/shadow_dom_component_factory.dart b/lib/core_dom/shadow_dom_component_factory.dart index d548b5469..ea7fa1b63 100644 --- a/lib/core_dom/shadow_dom_component_factory.dart +++ b/lib/core_dom/shadow_dom_component_factory.dart @@ -117,17 +117,17 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory { shadowBoundary = new ShadowRootBoundary(shadowRoot); } - var shadowScope = scope.createChild(new HashMap()); // Isolate List futures = []; TemplateLoader templateLoader = new TemplateLoader(shadowRoot, futures); var probe; var eventHandler = new ShadowRootEventHandler( shadowRoot, injector.getByKey(EXPANDO_KEY), injector.getByKey(EXCEPTION_HANDLER_KEY)); - final shadowInjector = new ComponentDirectiveInjector(injector, _injector, eventHandler, shadowScope, - templateLoader, shadowRoot, null, view, shadowBoundary); + final shadowInjector = new ComponentDirectiveInjector(injector, _injector, eventHandler, null, + templateLoader, shadowRoot, null, _ref.typeKey, view, shadowBoundary); shadowInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); + if (_component.useNgBaseCss && baseCss.urls.isNotEmpty) { if (baseCss.styles == null) { final f = _componentFactory.cssLoader(_tag, baseCss.urls).then((cssList) { @@ -152,26 +152,26 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory { if (_shadowViewFactoryFuture != null) { if (_shadowViewFactory == null) { final f = _shadowViewFactoryFuture.then((ViewFactory viewFactory) => - _insertView(viewFactory, shadowRoot, shadowScope, shadowInjector)); + _insertView(viewFactory, shadowRoot, shadowInjector.scope, shadowInjector)); futures.add(f); } else { final f = new Future.microtask(() { - _insertView(_shadowViewFactory, shadowRoot, shadowScope, shadowInjector); + _insertView(_shadowViewFactory, shadowRoot, shadowInjector.scope, shadowInjector); }); futures.add(f); } } + var controller = shadowInjector.getByKey(_ref.typeKey); + var shadowScope = shadowInjector.getByKey(SCOPE_KEY); + if (controller is ScopeAware) controller.scope = shadowScope; + BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + if (_componentFactory.config.elementProbeEnabled) { ElementProbe probe = _componentFactory.expando[shadowRoot] = shadowInjector.elementProbe; shadowScope.on(ScopeEvent.DESTROY).listen((ScopeEvent) => _componentFactory.expando[shadowRoot] = null); } - var controller = shadowInjector.getByKey(_ref.typeKey); - if (controller is ScopeAware) controller.scope = shadowScope; - BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); - shadowScope.context[_component.publishAs] = controller; - return controller; } finally { traceLeave(s); diff --git a/lib/core_dom/shadow_dom_component_factory.dart.orig b/lib/core_dom/shadow_dom_component_factory.dart.orig new file mode 100644 index 000000000..85124bbf9 --- /dev/null +++ b/lib/core_dom/shadow_dom_component_factory.dart.orig @@ -0,0 +1,168 @@ +part of angular.core.dom_internal; + +abstract class ComponentFactory { + BoundComponentFactory bind(DirectiveRef ref, directives, Injector injector); +} + +/** + * A Component factory with has been bound to a specific component type. + */ +abstract class BoundComponentFactory { + List get callArgs; + Function call(dom.Element element); + + static async.Future _viewFuture( + Component component, ViewCache viewCache, DirectiveMap directives, + TypeToUriMapper uriMapper, ResourceUrlResolver resourceResolver, Type type) { + + if (component.template != null) { + // TODO(chirayu): Replace this line with + // var baseUri = uriMapper.uriForType(type); + // once we have removed _NullUriMapper. + var baseUriString = resourceResolver.combineWithType(type, null); + var baseUri = (baseUriString != null) ? Uri.parse(baseUriString) : null; + return new async.Future.value(viewCache.fromHtml(component.template, directives, baseUri)); + } + if (component.templateUrl != null) { + var url = resourceResolver.combineWithType(type, component.templateUrl); + var baseUri = Uri.parse(url); + return viewCache.fromUrl(url, directives, baseUri); + } + return null; + } + + static void _setupOnShadowDomAttach(component, TemplateLoader templateLoader, + Scope shadowScope) { + if (component is ShadowRootAware) { + templateLoader.template.then((shadowDom) { + if (!shadowScope.isAttached) return; + (component as ShadowRootAware).onShadowRoot(shadowDom); + }); + } + } +} + +@Injectable() +class ShadowDomComponentFactory implements ComponentFactory { + final ViewCache viewCache; + final PlatformJsBasedShim platformShim; + final Expando expando; + final CompilerConfig config; + final TypeToUriMapper uriMapper; + final ResourceUrlResolver resourceResolver; + + ComponentCssLoader cssLoader; + + ShadowDomComponentFactory(this.viewCache, this.platformShim, this.expando, this.config, + this.uriMapper, this.resourceResolver, Http http, TemplateCache templateCache, + ComponentCssRewriter componentCssRewriter, dom.NodeTreeSanitizer treeSanitizer, + CacheRegister cacheRegister) { + final styleElementCache = new HashMap(); + cacheRegister.registerCache("ShadowDomComponentFactoryStyles", styleElementCache); + + cssLoader = new ComponentCssLoader(http, templateCache, platformShim, + componentCssRewriter, treeSanitizer, styleElementCache, resourceResolver); + } + + bind(DirectiveRef ref, directives, injector) => + new BoundShadowDomComponentFactory(this, ref, directives, injector); +} + +class BoundShadowDomComponentFactory implements BoundComponentFactory { + + final ShadowDomComponentFactory _componentFactory; + final DirectiveRef _ref; + final Injector _injector; + + Component get _component => _ref.annotation as Component; + + String _tag; + async.Future> _styleElementsFuture; + async.Future _viewFuture; + + BoundShadowDomComponentFactory(this._componentFactory, this._ref, + DirectiveMap directives, this._injector) { + _tag = _ref.annotation.selector.toLowerCase(); + _styleElementsFuture = _componentFactory.cssLoader(_tag, _component.cssUrls, type: _ref.type); + + final viewCache = new ShimmingViewCache(_componentFactory.viewCache, + _tag, _componentFactory.platformShim); + _viewFuture = BoundComponentFactory._viewFuture(_component, viewCache, directives, + _componentFactory.uriMapper, _componentFactory.resourceResolver, _ref.type); + } + + List get callArgs => _CALL_ARGS; + static final _CALL_ARGS = [DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, VIEW_KEY, NG_BASE_CSS_KEY, + SHADOW_BOUNDARY_KEY]; + Function call(dom.Element element) { + return (DirectiveInjector injector, Scope scope, View view, NgBaseCss baseCss, + ShadowBoundary parentShadowBoundary) { + var s = traceEnter(View_createComponent); + try { + var shadowDom = element.createShadowRoot(); + + var shadowBoundary; + if (_componentFactory.platformShim.shimRequired) { + shadowBoundary = parentShadowBoundary; + } else { + shadowBoundary = new ShadowRootBoundary(shadowDom); + } + + Scope shadowScope; + ComponentDirectiveInjector shadowInjector; + + final baseUrls = (_component.useNgBaseCss) ? baseCss.urls : []; + final baseUrlsFuture = _componentFactory.cssLoader(_tag, baseUrls); + final cssFuture = mergeFutures(baseUrlsFuture, _styleElementsFuture); + + async.Future initShadowDom(_) { + if (_viewFuture == null) return new async.Future.value(shadowDom); + return _viewFuture.then((ViewFactory viewFactory) { + if (shadowScope.isAttached) { + shadowDom.nodes.addAll( + viewFactory.call(shadowInjector.scope, shadowInjector).nodes); + } + return shadowDom; + }); + } + + TemplateLoader templateLoader = new TemplateLoader( + cssFuture.then(shadowBoundary.insertStyleElements).then(initShadowDom)); + + var probe; + var eventHandler = new ShadowRootEventHandler( + shadowDom, injector.getByKey(EXPANDO_KEY), injector.getByKey(EXCEPTION_HANDLER_KEY)); + shadowInjector = new ComponentDirectiveInjector(injector, _injector, eventHandler, shadowScope, + templateLoader, shadowDom, null, _ref.typeKey, view, shadowBoundary); + + + shadowInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); + + var controller = shadowInjector.getByKey(_ref.typeKey); + shadowScope = shadowInjector.getByKey(SCOPE_KEY); + BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + + if (_componentFactory.config.elementProbeEnabled) { + probe = _componentFactory.expando[shadowDom] = shadowInjector.elementProbe; + shadowScope.on(ScopeEvent.DESTROY).listen((ScopeEvent) => _componentFactory.expando[shadowDom] = null); + } + +<<<<<<< HEAD + var controller = shadowInjector.getByKey(_ref.typeKey); + BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + shadowScope.context[_component.publishAs] = controller; +======= +>>>>>>> feat(scope): component is the new context + + return controller; + } finally { + traceLeave(s); + } + }; + } +} + +@Injectable() +class ComponentCssRewriter { + String call(String css, {String selector, String cssUrl}) => css; +} diff --git a/lib/core_dom/transcluding_component_factory.dart b/lib/core_dom/transcluding_component_factory.dart index 63d18a194..d9293a55e 100644 --- a/lib/core_dom/transcluding_component_factory.dart +++ b/lib/core_dom/transcluding_component_factory.dart @@ -76,12 +76,11 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory { final shadowRoot = new EmulatedShadowRoot(element); final lightDom = new LightDom(element, scope)..pullNodes(); - final shadowScope = scope.createChild(new HashMap()); List futures = []; TemplateLoader templateLoader = new TemplateLoader(shadowRoot, futures); final childInjector = new ComponentDirectiveInjector(injector, this._injector, - eventHandler, shadowScope, templateLoader, shadowRoot, lightDom, view); + eventHandler, null, templateLoader, shadowRoot, lightDom, _ref.typeKey, view); childInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); if (_component.useNgBaseCss && baseCss.urls.isNotEmpty) { @@ -122,7 +121,7 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory { } var controller = childInjector.getByKey(_ref.typeKey); - shadowScope.context[_component.publishAs] = controller; + Scope shadowScope = childInjector.getByKey(SCOPE_KEY); if (controller is ScopeAware) controller.scope = shadowScope; BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); diff --git a/lib/core_dom/transcluding_component_factory.dart.orig b/lib/core_dom/transcluding_component_factory.dart.orig new file mode 100644 index 000000000..59a7a0be2 --- /dev/null +++ b/lib/core_dom/transcluding_component_factory.dart.orig @@ -0,0 +1,122 @@ +part of angular.core.dom_internal; + +@Injectable() +class TranscludingComponentFactory implements ComponentFactory { + + final Expando expando; + final ViewCache viewCache; + final CompilerConfig config; + final DefaultPlatformShim platformShim; + final TypeToUriMapper uriMapper; + final ResourceUrlResolver resourceResolver; + ComponentCssLoader cssLoader; + + TranscludingComponentFactory(this.expando, this.viewCache, this.config, this.platformShim, + this.uriMapper, this.resourceResolver, Http http, TemplateCache templateCache, + ComponentCssRewriter componentCssRewriter, dom.NodeTreeSanitizer treeSanitizer, + CacheRegister cacheRegister) { + final styleElementCache = new HashMap(); + cacheRegister.registerCache("TranscludingComponentFactoryStyles", styleElementCache); + + cssLoader = new ComponentCssLoader(http, templateCache, platformShim, + componentCssRewriter, treeSanitizer, styleElementCache, resourceResolver); + } + + bind(DirectiveRef ref, directives, injector) => + new BoundTranscludingComponentFactory(this, ref, directives, injector); +} + +class BoundTranscludingComponentFactory implements BoundComponentFactory { + final TranscludingComponentFactory _f; + final DirectiveRef _ref; + final DirectiveMap _directives; + final Injector _injector; + + String _tag; + async.Future> _styleElementsFuture; + + Component get _component => _ref.annotation as Component; + async.Future _viewFuture; + + BoundTranscludingComponentFactory(this._f, this._ref, this._directives, this._injector) { + _viewFuture = BoundComponentFactory._viewFuture( + _component, + _f.viewCache, + _directives, + _f.uriMapper, + _f.resourceResolver, + _ref.type); + + _tag = _ref.annotation.selector.toLowerCase(); + _styleElementsFuture = _f.cssLoader(_tag, _component.cssUrls, type: _ref.type); + + final viewCache = new ShimmingViewCache(_f.viewCache, _tag, _f.platformShim); + _viewFuture = BoundComponentFactory._viewFuture(_component, viewCache, _directives, + _f.uriMapper, _f.resourceResolver, _ref.type); + } + + List get callArgs => _CALL_ARGS; + static var _CALL_ARGS = [ DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, VIEW_KEY, + VIEW_CACHE_KEY, HTTP_KEY, TEMPLATE_CACHE_KEY, + DIRECTIVE_MAP_KEY, NG_BASE_CSS_KEY, EVENT_HANDLER_KEY, + SHADOW_BOUNDARY_KEY]; + Function call(dom.Node node) { + var element = node as dom.Element; + return (DirectiveInjector injector, Scope scope, View view, + ViewCache viewCache, Http http, TemplateCache templateCache, + DirectiveMap directives, NgBaseCss baseCss, EventHandler eventHandler, + ShadowBoundary shadowBoundary) { + + DirectiveInjector childInjector; + var childInjectorCompleter; // Used if the ViewFuture is available before the childInjector. + + var component = _component; + final shadowRoot = new EmulatedShadowRoot(element); + var lightDom = new LightDom(element, scope)..pullNodes(); + + final baseUrls = (_component.useNgBaseCss) ? baseCss.urls : []; + final baseUrlsFuture = _f.cssLoader(_tag, baseUrls); + final cssFuture = mergeFutures(baseUrlsFuture, _styleElementsFuture); + + initShadowDom(_) { + if (_viewFuture != null) { + return _viewFuture.then((ViewFactory viewFactory) { + lightDom.clearComponentElement(); + if (childInjector != null) { + lightDom.shadowDomView = viewFactory.call(childInjector.scope, childInjector); + return shadowRoot; + } else { + childInjectorCompleter = new async.Completer(); + return childInjectorCompleter.future.then((childInjector) { + lightDom.shadowDomView = viewFactory.call(childInjector.scope, childInjector); + return shadowRoot; + }); + } + }); + } else { + return new async.Future.microtask(lightDom.clearComponentElement); + } + } + + TemplateLoader templateLoader = new TemplateLoader( + cssFuture.then(shadowBoundary.insertStyleElements).then(initShadowDom)); + + childInjector = new ComponentDirectiveInjector(injector, this._injector, + eventHandler, null, templateLoader, shadowRoot, lightDom, _ref.typeKey, + view); + + childInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); + + if (childInjectorCompleter != null) childInjectorCompleter.complete(childInjector); + + var controller = childInjector.getByKey(_ref.typeKey); +<<<<<<< HEAD + shadowScope.context[component.publishAs] = controller; +======= + Scope shadowScope = childInjector.getByKey(SCOPE_KEY); +>>>>>>> feat(scope): component is the new context + BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + return controller; + }; + } +} diff --git a/lib/core_dom/view.dart.orig b/lib/core_dom/view.dart.orig new file mode 100644 index 000000000..70d9f7dd0 --- /dev/null +++ b/lib/core_dom/view.dart.orig @@ -0,0 +1,119 @@ +part of angular.core.dom_internal; + +/** + * A View is a fundamental building block of DOM. It is a chunk of DOM which + * can not be structurally changed. A View can have [ViewPort] placeholders + * embedded in its DOM. A [ViewPort] can contain other [View]s and it is the + * only way in which DOM structure can be modified. + * + * A [View] is a collection of DOM nodes + + * A [View] can be created from [ViewFactory]. + * + */ +class View { + final Scope scope; + final List nodes; + final List insertionPoints = []; + + View(this.nodes, this.scope); + + void addViewPort(ViewPort viewPort) { + insertionPoints.add(viewPort); + } + + void addContent(Content content) { + insertionPoints.add(content); + } + + void domWrite(fn()) { + scope.domWrite(fn); + } + + void domRead(fn()) { + scope.domRead(fn); + } +} + +/** + * A ViewPort maintains an ordered list of [View]'s. It contains a + * [placeholder] node that is used as the insertion point for view nodes. + */ +class ViewPort { + final DirectiveInjector directiveInjector; + final Scope scope; + final dom.Node placeholder; + final Animate _animate; + final DestinationLightDom _lightDom; + final View _parentView; + final views = []; + + ViewPort(DirectiveInjector directiveInjector, this.scope, this.placeholder, this._animate, [this._lightDom, View parentView]) + : directiveInjector = directiveInjector, + _parentView = parentView != null ? parentView : directiveInjector.getByKey(VIEW_KEY) { + _parentView.addViewPort(this); + } + + View insertNew(ViewFactory viewFactory, {View insertAfter, Scope viewScope}) { +<<<<<<< HEAD + if (viewScope == null) viewScope = scope.createProtoChild(); +======= + if (viewScope == null) viewScope = scope.createChild(scope.context); +>>>>>>> feat(scope): component is the new context + View view = viewFactory.call(viewScope, directiveInjector); + return insert(view, insertAfter: insertAfter); + } + + View insert(View view, { View insertAfter }) { + scope.rootScope.domWrite(() { + dom.Node previousNode = _lastNode(insertAfter); + _viewsInsertAfter(view, insertAfter); + _animate.insert(view.nodes, placeholder.parentNode, insertBefore: previousNode.nextNode); + _notifyLightDom(); + }); + return view; + } + + View remove(View view) { + view.scope.destroy(); + views.remove(view); + scope.rootScope.domWrite(() { + _animate.remove(view.nodes); + _notifyLightDom(); + }); + return view; + } + + View move(View view, { View moveAfter }) { + dom.Node previousNode = _lastNode(moveAfter); + views.remove(view); + _viewsInsertAfter(view, moveAfter); + scope.rootScope.domWrite(() { + _animate.move(view.nodes, placeholder.parentNode, insertBefore: previousNode.nextNode); + _notifyLightDom(); + }); + return view; + } + + void _viewsInsertAfter(View view, View insertAfter) { + int index = insertAfter == null ? 0 : views.indexOf(insertAfter) + 1; + views.insert(index, view); + } + + List get nodes { + final r = []; + for(final v in views) { + r.addAll(v.nodes); + } + return r; + } + + void _notifyLightDom() { + if (_lightDom != null) _lightDom.redistribute(); + } + + dom.Node _lastNode(View insertAfter) => + insertAfter == null + ? placeholder + : insertAfter.nodes.last; +} diff --git a/lib/directive/module.dart b/lib/directive/module.dart index 8b903d3cf..69a29d7a9 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -12,7 +12,8 @@ * * For example: * - * this text is conditionally visible + * this text is conditionally visible + * */ library angular.directive; diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 4f607d0e2..9a6c4216b 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -173,7 +173,8 @@ abstract class _NgClassBase { nodeAttrs.observe('class', (String cls) { if (prevCls != cls) { prevCls = cls; - _applyChanges(_scope.context[r'$index']); + var index = _hasLocal(_scope, r'$index') ? _getLocal(_scope, r'$index') : null; + _applyChanges(index); } }); } @@ -182,7 +183,8 @@ abstract class _NgClassBase { if (_watchExpression != null) _watchExpression.remove(); _watchExpression = _scope.watch(expression, (v, _) { _computeChanges(v); - _applyChanges(_scope.context[r'$index']); + var index = _hasLocal(_scope, r'$index') ? _getLocal(_scope, r'$index') : null; + _applyChanges(index); }, canChangeModel: false, collection: true); @@ -278,3 +280,21 @@ abstract class _NgClassBase { _previousSet = _currentSet.toSet(); } } + +bool _hasLocal(context, name) { + var ctx = context; + while (ctx is ContextLocals) { + if (ctx.hasProperty(name)) return true; + ctx = ctx.parentScope; + } + return false; +} + +dynamic _getLocal(context, name) { + var ctx = context; + while (ctx is ContextLocals) { + if (ctx.hasProperty(name)) return ctx[name]; + ctx = ctx.parentScope; + } + return null; +} diff --git a/lib/directive/ng_form.dart b/lib/directive/ng_form.dart index 595d547a2..57c693d49 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -17,7 +17,7 @@ part of angular.directive; @Decorator( selector: '[ng-form]', module: NgForm.module, - map: const { 'ng-form': '@name' }) + map: const { 'ng-form': '&name' }) class NgForm extends NgControl { static module(DirectiveBinder binder) => binder.bind(NgControl, toInstanceOf: NG_FORM_KEY, visibility: Visibility.CHILDREN); @@ -52,19 +52,26 @@ class NgForm extends NgControl { * The name of the control. This is usually fetched via the name attribute that is * present on the element that the control is bound to. */ - @NgAttr('name') - get name => _name; - set name(String value) { - if (value != null) { - super.name = value; - _scope.context[name] = this; - } - } + @NgCallback('name') + String get name => super.name; + void set name(exp) { + // The type could not be added on the parameter as the base setter takes a String + assert(exp is BoundExpression); + var name = exp.expression.toString(); + if (name != null && name.isNotEmpty) { + super.name = name; + try { + exp.assign(this); + } catch (e) { + throw 'There must be a "$name" field on your component to store the form instance.'; + } + } + } /** * The list of associated child controls. */ - get controls => _controlByName; + Map> get controls => _controlByName; /** * Returns the child control that is associated with the given name. If multiple diff --git a/lib/directive/ng_include.dart b/lib/directive/ng_include.dart index 6be82af14..4bd00a122 100644 --- a/lib/directive/ng_include.dart +++ b/lib/directive/ng_include.dart @@ -45,7 +45,6 @@ class NgInclude { // create a new scope _childScope = scope.createProtoChild(); _view = viewFactory(_childScope, directiveInjector); - _view.nodes.forEach((node) => element.append(node)); } diff --git a/lib/directive/ng_include.dart.orig b/lib/directive/ng_include.dart.orig new file mode 100644 index 000000000..f458f6cf9 --- /dev/null +++ b/lib/directive/ng_include.dart.orig @@ -0,0 +1,61 @@ +part of angular.directive; + +/** + * Fetches, compiles and includes an external Angular template/HTML. + * + * A new child [Scope] is created for the included DOM subtree. + * + * [NgInclude] provides only one small part of the power of + * [Component].  Consider using directives and components instead as they + * provide this feature as well as much more. + * + * Note: The browser's Same Origin Policy () and + * Cross-Origin Resource Sharing (CORS) policy () restrict + * whether the template is successfully loaded.  For example, + * [NgInclude] won't work for cross-domain requests on all browsers and + * for `file://` access on some browsers. + */ +@Decorator( + selector: '[ng-include]', + map: const {'ng-include': '@url'}) +class NgInclude { + + final dom.Element element; + final Scope scope; + final ViewCache viewCache; + final DirectiveInjector directiveInjector; + final DirectiveMap directives; + + View _view; + Scope _childScope; + + NgInclude(this.element, this.scope, this.viewCache, + this.directiveInjector, this.directives); + + _cleanUp() { + if (_view == null) return; + _view.nodes.forEach((node) => node.remove); + _childScope.destroy(); + _childScope = null; + element.innerHtml = ''; + _view = null; + } + + _updateContent(ViewFactory viewFactory) { + // create a new scope +<<<<<<< HEAD + _childScope = scope.createProtoChild(); +======= + _childScope = scope.createChild(scope.context); +>>>>>>> feat(scope): component is the new context + _view = viewFactory(_childScope, directiveInjector); + _view.nodes.forEach((node) => element.append(node)); + } + + set url(value) { + _cleanUp(); + if (value != null && value != '') { + viewCache.fromUrl(value, directives, Uri.base).then(_updateContent); + } + } +} diff --git a/lib/directive/ng_repeat.dart b/lib/directive/ng_repeat.dart index 951830c98..15aeb38cb 100644 --- a/lib/directive/ng_repeat.dart +++ b/lib/directive/ng_repeat.dart @@ -111,8 +111,7 @@ class NgRepeat { ..[r'$index'] = index ..[r'$id'] = (obj) => obj; if (_keyIdentifier != null) context[_keyIdentifier] = key; - return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context, - context)); + return relaxFnArgs(trackBy.eval)(new ContextLocals(_scope.context, context)); }); } @@ -171,10 +170,11 @@ class NgRepeat { }); addFn((CollectionChangeItem addition) { + var value = addition.item; changeFunctions[addition.currentIndex] = (index, previousView) { var childScope = _scope.createProtoChild(); var childContext = _updateContext(childScope.context, index, length) - ..[_valueIdentifier] = addition.item; + ..[_valueIdentifier] = value; var view = views[index] = _boundViewFactory(childScope); _viewPort.insert(view, insertAfter: previousView); }; @@ -222,9 +222,10 @@ class NgRepeat { _views = views; } - Map _updateContext(Map context, int index, int length) { - var first = (index == 0); - var last = (index == length - 1); + ContextLocals _updateContext(ContextLocals context, int index, int len) { + var first = index == 0; + var last = index == len - 1; + return context ..[r'$index'] = index ..[r'$first'] = first diff --git a/lib/directive/ng_repeat.dart.orig b/lib/directive/ng_repeat.dart.orig new file mode 100644 index 000000000..20dfa95d0 --- /dev/null +++ b/lib/directive/ng_repeat.dart.orig @@ -0,0 +1,250 @@ +part of angular.directive; + +/** + * The `ngRepeat` directive instantiates a template once per item from a + * collection. Each template instance gets its own scope, where the given loop + * variable is set to the current collection item, and `$index` is set to the + * item index or key. + * + * Special properties are exposed on the local scope of each template instance, + * including: + * + * * `$index` ([:num:]) the iterator offset of the repeated element + * (0..length-1) + * * `$first` ([:bool:]) whether the repeated element is first in the + * iterator. + * * `$middle` ([:bool:]) whether the repeated element is between the first + * and last in the iterator. + * * `$last` ([:bool:]) whether the repeated element is last in the iterator. + * * `$even` ([:bool:]) whether the iterator position `$index` is even. + * * `$odd` ([:bool:]) whether the iterator position `$index` is odd. + * + * + * [repeat_expression] ngRepeat The expression indicating how to enumerate a + * collection. These formats are currently supported: + * + * * `variable in expression` – where variable is the user defined loop + * variable and `expression` is a scope expression giving the collection to + * enumerate. + * + * For example: `album in artist.albums`. + * + * * `variable in expression track by tracking_expression` – You can also + * provide an optional tracking function which can be used to associate the + * objects in the collection with the DOM elements. If no tracking function is + * specified the ng-repeat associates elements by identity in the collection. + * It is an error to have more than one tracking function to resolve to the + * same key. (This would mean that two distinct objects are mapped to the same + * DOM element, which is not possible.) Formatters should be applied to the + * expression, before specifying a tracking expression. + * + * For example: `item in items` is equivalent to `item in items track by + * $id(item)`. This implies that the DOM elements will be associated by item + * identity in the array. + * + * For example: `item in items track by $id(item)`. A built in `$id()` + * function can be used to assign a unique `$$hashKey` property to each item + * in the array. This property is then used as a key to associated DOM + * elements with the corresponding item in the array by identity. Moving the + * same object in array would move the DOM element in the same way in the + * DOM. + * + * For example: `item in items track by item.id` is a typical pattern when + * the items come from the database. In this case the object identity does + * not matter. Two objects are considered equivalent as long as their `id` + * property is same. + * + * For example: `item in items | filter:searchText track by item.id` is a + * pattern that might be used to apply a formatter to items in conjunction with + * a tracking expression. + * + * # Example: + * + *
    + *
  • {{item}}
  • + *
+ */ + +@Decorator( + children: Directive.TRANSCLUDE_CHILDREN, + selector: '[ng-repeat]', + map: const {'.': '@expression'}) +class NgRepeat { + static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(?:track\s+by\s+(.+)\s*)?(\s+lazily\s*)?$'); + static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([$\w]+)|\(([$\w]+)\s*,\s*([$\w]+)\))$'); + + final ViewPort _viewPort; + final BoundViewFactory _boundViewFactory; + final Scope _scope; + final Parser _parser; + final FormatterMap formatters; + + String _expression; + String _valueIdentifier; + String _keyIdentifier; + String _listExpr; + List _views; + Function _generateId = (key, value, index) => value; + Watch _watch; + + NgRepeat(this._viewPort, this._boundViewFactory, this._scope, this._parser, this.formatters); + + set expression(value) { + assert(value != null); + _expression = value; + if (_watch != null) _watch.remove(); + + Match match = _SYNTAX.firstMatch(_expression); + if (match == null) { + throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ " + "in _collection_[ track by _id_]' but got '$_expression'."; + } + + _listExpr = match.group(2); + + var trackByExpr = match.group(3); + if (trackByExpr != null) { + Expression trackBy = _parser(trackByExpr); + _generateId = ((key, value, index) { + final context = new HashMap() + ..[_valueIdentifier] = value + ..[r'$index'] = index + ..[r'$id'] = (obj) => obj; + if (_keyIdentifier != null) context[_keyIdentifier] = key; + return relaxFnArgs(trackBy.eval)(new ContextLocals(_scope.context, context)); + }); + } + + var assignExpr = match.group(1); + match = _LHS_SYNTAX.firstMatch(assignExpr); + if (match == null) { + throw "[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' " + "should be an identifier or '(_key_, _value_)' expression, but got " + "'$assignExpr'."; + } + + _valueIdentifier = match.group(3); + if (_valueIdentifier == null) _valueIdentifier = match.group(1); + _keyIdentifier = match.group(2); + + _watch = _scope.watch( + _listExpr, + (changes, _) { + if (changes is CollectionChangeRecord && changes != null) { + _onCollectionChange(changes); + } else if (_views != null) { + _views.forEach(_viewPort.remove); + _views = null; + } + }, + collection: true, + formatters: formatters + ); + } + + void _onCollectionChange(CollectionChangeRecord changes) { + final int length = changes.length; + final views = new List(length); + final changeFunctions = new List(length); + final removedIndexes = []; + final int domLength = _views == null ? 0 : _views.length; + final leftInDom = new List.generate(domLength, (i) => domLength - 1 - i); + var domIndex; + + Function addFn, moveFn, removeFn; + + if (_views == null) { + addFn = changes.forEachItem; + moveFn = (_) {}; + removeFn = (_) {}; + } else { + addFn = changes.forEachAddition; + moveFn = changes.forEachMove; + removeFn = changes.forEachRemoval; + } + + removeFn((CollectionChangeItem removal) { + var index = removal.previousIndex; + _viewPort.remove(_views[index]); + leftInDom.removeAt(domLength - 1 - index); + }); + + addFn((CollectionChangeItem addition) { + var value = addition.item; + changeFunctions[addition.currentIndex] = (index, previousView) { +<<<<<<< HEAD + var childScope = _scope.createProtoChild(); + var childContext = _updateContext(childScope.context, index, length) + ..[_valueIdentifier] = addition.item; +======= + var childContext = new ContextLocals(_scope.context); + childContext = _updateContext(childContext, index, length); + childContext[_valueIdentifier] = value; + var childScope = _scope.createChild(childContext); +>>>>>>> feat(scope): component is the new context + var view = views[index] = _boundViewFactory(childScope); + _viewPort.insert(view, insertAfter: previousView); + }; + }); + + moveFn((CollectionChangeItem move) { + var previousIndex = move.previousIndex; + var value = move.item; + changeFunctions[move.currentIndex] = (index, moveAfter) { + var previousView = _views[previousIndex]; + var childScope = previousView.scope; + var childContext = _updateContext(childScope.context, index, length); + if (!identical(childScope.context[_valueIdentifier], value)) { + childContext[_valueIdentifier] = value; + } + views[index] = _views[previousIndex]; + // Only move the DOM node when required + if (domIndex < 0 || leftInDom[domIndex] != previousIndex) { + _viewPort.move(previousView, moveAfter: moveAfter); + leftInDom.remove(previousIndex); + } + domIndex--; + }; + }); + + var previousView = null; + domIndex = leftInDom.length - 1; + for(var targetIndex = 0; targetIndex < length; targetIndex++) { + var changeFn = changeFunctions[targetIndex]; + if (changeFn == null) { + views[targetIndex] = _views[targetIndex]; + if (domIndex < 0 || leftInDom[domIndex] != targetIndex) { + _viewPort.move(views[targetIndex], moveAfter: previousView); + leftInDom.remove(targetIndex); + } + domIndex--; + // The element has not moved but `$last` and `$middle` might still need to be updated + _updateContext(views[targetIndex].scope.context, targetIndex, length); + } else { + changeFn(targetIndex, previousView); + } + previousView = views[targetIndex]; + } + + _views = views; + } + +<<<<<<< HEAD + Map _updateContext(Map context, int index, int length) { + var first = (index == 0); + var last = (index == length - 1); +======= + ContextLocals _updateContext(ContextLocals context, int index, int len) { + var first = index == 0; + var last = index == len - 1; + +>>>>>>> feat(scope): component is the new context + return context + ..[r'$index'] = index + ..[r'$first'] = first + ..[r'$last'] = last + ..[r'$middle'] = !(first || last) + ..[r'$odd'] = index.isOdd + ..[r'$even'] = index.isEven; + } +} diff --git a/lib/directive/ng_switch.dart b/lib/directive/ng_switch.dart index 9953b6b3f..d618636e1 100644 --- a/lib/directive/ng_switch.dart +++ b/lib/directive/ng_switch.dart @@ -57,34 +57,31 @@ part of angular.directive; }, visibility: Directive.DIRECT_CHILDREN_VISIBILITY) class NgSwitch { - Map> cases = new Map>(); - List<_ViewScopePair> currentViews = <_ViewScopePair>[]; + final _cases = >{'?': <_Case>[]}; + final _currentViews = <_ViewScopePair>[]; Function onChange; - final Scope scope; + final Scope _scope; - NgSwitch(this.scope) { - cases['?'] = <_Case>[]; - } + NgSwitch(this._scope); - addCase(String value, ViewPort anchor, BoundViewFactory viewFactory) { - cases.putIfAbsent(value, () => <_Case>[]); - cases[value].add(new _Case(anchor, viewFactory)); + void addCase(String value, ViewPort anchor, BoundViewFactory viewFactory) { + _cases.putIfAbsent(value, () => <_Case>[]).add(new _Case(anchor, viewFactory)); } set value(val) { - currentViews + _currentViews ..forEach((_ViewScopePair pair) { pair.port.remove(pair.view); }) ..clear(); val = '!$val'; - (cases.containsKey(val) ? cases[val] : cases['?']) + (_cases.containsKey(val) ? _cases[val] : _cases['?']) .forEach((_Case caze) { - Scope childScope = scope.createProtoChild(); + Scope childScope = _scope.createProtoChild(); var view = caze.viewFactory(childScope); caze.anchor.insert(view); - currentViews.add(new _ViewScopePair(view, caze.anchor, + _currentViews.add(new _ViewScopePair(view, caze.anchor, childScope)); }); if (onChange != null) { @@ -113,23 +110,20 @@ class _Case { children: Directive.TRANSCLUDE_CHILDREN, map: const {'.': '@value'}) class NgSwitchWhen { - final NgSwitch ngSwitch; - final ViewPort port; - final BoundViewFactory viewFactory; - final Scope scope; + final NgSwitch _ngSwitch; + final ViewPort _port; + final BoundViewFactory _viewFactory; - NgSwitchWhen(this.ngSwitch, this.port, this.viewFactory, this.scope); + NgSwitchWhen(this._ngSwitch, this._port, this._viewFactory); - set value(String value) => ngSwitch.addCase('!$value', port, viewFactory); + void set value(String value) => _ngSwitch.addCase('!$value', _port, _viewFactory); } @Decorator( children: Directive.TRANSCLUDE_CHILDREN, selector: '[ng-switch-default]') class NgSwitchDefault { - - NgSwitchDefault(NgSwitch ngSwitch, ViewPort port, - BoundViewFactory viewFactory, Scope scope) { + NgSwitchDefault(NgSwitch ngSwitch, ViewPort port, BoundViewFactory viewFactory) { ngSwitch.addCase('?', port, viewFactory); } } diff --git a/lib/directive/ng_switch.dart.orig b/lib/directive/ng_switch.dart.orig new file mode 100644 index 000000000..215bd3a7d --- /dev/null +++ b/lib/directive/ng_switch.dart.orig @@ -0,0 +1,156 @@ +part of angular.directive; + +/** + * The ngSwitch directive is used to conditionally swap DOM structure on your + * template based on a scope expression. Elements within ngSwitch but without + * ngSwitchWhen or ngSwitchDefault directives will be preserved at the location + * as specified in the template. + * + * The directive itself works similar to ngInclude, however, instead of + * downloading template code (or loading it from the template cache), ngSwitch + * simply choses one of the nested elements and makes it visible based on which + * element matches the value obtained from the evaluated expression. In other + * words, you define a container element (where you place the directive), place + * an expression on the **ng-switch="..." attribute**, define any inner elements + * inside of the directive and place a when attribute per element. The when + * attribute is used to inform ngSwitch which element to display when the on + * expression is evaluated. If a matching expression is not found via a when + * attribute then an element with the default attribute is displayed. + * + * ## Example: + * + * + * ... + * ... + * ... + * + * + * On child elements add: + * + * * `ngSwitchWhen`: the case statement to match against. If match then this + * case will be displayed. If the same match appears multiple times, all the + * elements will be displayed. + * * `ngSwitchDefault`: the default case when no other case match. If there + * are multiple default cases, all of them will be displayed when no other + * case match. + * + * ## Example: + * + *
+ * + * + * + * selection={{selection}} + *
+ *
+ *
Settings Div
+ *
Home Span
+ *
default
+ *
+ *
+ */ +@Decorator( + selector: '[ng-switch]', + map: const { + 'ng-switch': '=>value', + 'change': '&onChange' + }, + visibility: Directive.DIRECT_CHILDREN_VISIBILITY) +class NgSwitch { + final _cases = >{'?': <_Case>[]}; + final _currentViews = <_ViewRef>[]; + Function onChange; + final Scope _scope; + + NgSwitch(this._scope); + + void addCase(String value, ViewPort anchor, BoundViewFactory viewFactory) { + _cases.putIfAbsent(value, () => <_Case>[]).add(new _Case(anchor, viewFactory)); + } + + void set value(val) { + _currentViews..forEach((_ViewRef view) => view.remove()) + ..clear(); + + val = '!$val'; +<<<<<<< HEAD + (cases.containsKey(val) ? cases[val] : cases['?']) + .forEach((_Case caze) { + Scope childScope = scope.createProtoChild(); + var view = caze.viewFactory(childScope); + caze.anchor.insert(view); + currentViews.add(new _ViewScopePair(view, caze.anchor, + childScope)); + }); + if (onChange != null) { + onChange(); + } + } +} + +class _ViewScopePair { + final View view; + final ViewPort port; + final Scope scope; + + _ViewScopePair(this.view, this.port, this.scope); +} +======= + var cases = _cases.containsKey(val) ? _cases[val] : _cases['?']; + cases.forEach((_Case caze) { + var childScope = _scope.createChild(_scope.context); + _currentViews.add(caze.createView(childScope)); + }); +>>>>>>> feat(scope): component is the new context + + if (onChange != null) onChange(); + } +} + +@Decorator( + selector: '[ng-switch-when]', + children: Directive.TRANSCLUDE_CHILDREN, + map: const {'.': '@value'}) +class NgSwitchWhen { + final NgSwitch _ngSwitch; + final ViewPort _port; + final BoundViewFactory _viewFactory; + + NgSwitchWhen(this._ngSwitch, this._port, this._viewFactory); + + void set value(String value) => _ngSwitch.addCase('!$value', _port, _viewFactory); +} + +@Decorator( + children: Directive.TRANSCLUDE_CHILDREN, + selector: '[ng-switch-default]') +class NgSwitchDefault { + NgSwitchDefault(NgSwitch ngSwitch, ViewPort port, BoundViewFactory viewFactory) { + ngSwitch.addCase('?', port, viewFactory); + } +} + +class _ViewRef { + final View _view; + final ViewPort _port; + final Scope _scope; + + _ViewRef(this._view, this._port, this._scope); + + void remove() { + _port.remove(_view); + } +} + +class _Case { + final ViewPort port; + final BoundViewFactory viewFactory; + + _Case(this.port, this.viewFactory); + + _ViewRef createView(Scope scope) { + var view = viewFactory(scope); + port.insert(view); + return new _ViewRef(view, port, scope); + } +} diff --git a/lib/mock/test_bed.dart b/lib/mock/test_bed.dart index 0244070a7..f40899e0c 100644 --- a/lib/mock/test_bed.dart +++ b/lib/mock/test_bed.dart @@ -90,7 +90,7 @@ class TestBed { rootScope.apply(); } - getProbe(Node node) { + ElementProbe getProbe(Node node) { while (node != null) { ElementProbe probe = expando[node]; if (probe != null) return probe; @@ -99,7 +99,7 @@ class TestBed { throw 'Probe not found.'; } - getScope(Node node) => getProbe(node).scope; + Scope getScope(Node node) => getProbe(node).scope; String _handleWhitespace(html) { return html.split('\n') diff --git a/lib/routing/ng_view.dart b/lib/routing/ng_view.dart index 76e603312..a1db9a07f 100644 --- a/lib/routing/ng_view.dart +++ b/lib/routing/ng_view.dart @@ -1,13 +1,13 @@ part of angular.routing; /** - * A directive that works with the [Router] and loads the template associated - * with the current route. + * A directive that works with the [Router] and loads the template associated with the current + * route. * * * - * [NgViewDirective] can work with [NgViewDirective] to define nested views - * for hierarchical routes. For example: + * [NgViewDirective] can work with [NgViewDirective] to define nested views for hierarchical routes. + * For example: * * void initRoutes(Router router, RouteViewFactory views) { * views.configure({ @@ -122,7 +122,7 @@ class NgView implements DetachAware, RouteProvider { _cleanUp(); _childScope = _scope.createProtoChild(); _view = viewFactory(_childScope, _dirInjector); - _view.nodes.forEach((elm) => _element.append(elm)); + _view.nodes.forEach((el) => _element.append(el)); }); } @@ -131,8 +131,8 @@ class NgView implements DetachAware, RouteProvider { _view.nodes.forEach((node) => node.remove()); _childScope.destroy(); - _view = null; _childScope = null; + _view = null; } Route get route => _viewRoute; @@ -140,13 +140,13 @@ class NgView implements DetachAware, RouteProvider { String get routeName => _viewRoute.name; Map get parameters { - var res = new HashMap(); - var p = _viewRoute; - while (p != null) { - res.addAll(p.parameters); - p = p.parent; + var params = new HashMap(); + var route = _viewRoute; + while (route != null) { + params.addAll(route.parameters); + route = route.parent; } - return res; + return params; } /** * Creates a child injector that allows loading new directives, formatters and @@ -197,7 +197,6 @@ class NgView implements DetachAware, RouteProvider { * injected [RouteProvider] will be null. */ abstract class RouteProvider { - /** * Returns [Route] for current view. */ diff --git a/lib/routing/ng_view.dart.orig b/lib/routing/ng_view.dart.orig new file mode 100644 index 000000000..52478cd40 --- /dev/null +++ b/lib/routing/ng_view.dart.orig @@ -0,0 +1,218 @@ +part of angular.routing; + +/** + * A directive that works with the [Router] and loads the template associated with the current + * route. + * + * + * + * [NgViewDirective] can work with [NgViewDirective] to define nested views for hierarchical routes. + * For example: + * + * void initRoutes(Router router, RouteViewFactory views) { + * views.configure({ + * 'library': ngRoute( + * path: '/library', + * view: 'library.html', + * mount: { + * 'all': ngRoute( + * path: '/all', + * view: 'book_list.html'), + * 'book': ngRoute( + * path: '/:bookId', + * mount: { + * 'overview': ngRoute( + * path: '/overview', + * defaultRoute: true, + * view: 'book_overview.html'), + * 'read': ngRoute( + * path: '/read', + * view: 'book_read.html'), + * }) + * }) + * }); + * + * index.html: + * + * + * + * library.html: + * + *
+ *

Library!

+ * + * + *
+ * + * book_list.html: + * + * + */ +@Decorator( + selector: 'ng-view', + module: NgView.module, + visibility: Visibility.CHILDREN) +class NgView implements DetachAware, RouteProvider { + static void module(DirectiveBinder binder) => + binder.bind(RouteProvider, toInstanceOf: NG_VIEW_KEY, visibility: Visibility.CHILDREN); + + final NgRoutingHelper _locationService; + final ViewCache _viewCache; + final Injector _appInjector; + final DirectiveInjector _dirInjector; + final Element _element; + final Scope _scope; + RouteHandle _route; + + View _view; + Scope _childScope; + Route _viewRoute; + + NgView(this._element, this._viewCache, DirectiveInjector dirInjector, this._appInjector, + Router router, this._scope) + : _dirInjector = dirInjector, + _locationService = dirInjector.getByKey(NG_ROUTING_HELPER_KEY) + { + RouteProvider routeProvider = dirInjector.getFromParentByKey(NG_VIEW_KEY); + _route = routeProvider != null ? + routeProvider.route.newHandle() : + router.root.newHandle(); + _locationService._registerPortal(this); + _maybeReloadViews(); + } + + void _maybeReloadViews() { + if (_route.isActive) _locationService._reloadViews(startingFrom: _route); + } + + void detach() { + _route.discard(); + _locationService._unregisterPortal(this); + _cleanUp(); + } + + void _show(_View viewDef, Route route, List modules) { + assert(route.isActive); + + if (_viewRoute != null) return; + _viewRoute = route; + + StreamSubscription _leaveSubscription; + _leaveSubscription = route.onLeave.listen((_) { + _leaveSubscription.cancel(); + _leaveSubscription = null; + _viewRoute = null; + _cleanUp(); + }); + + Injector viewInjector = _appInjector; + + if (modules != null) { + viewInjector = createChildInjectorWithReload(_appInjector, modules); + } + + var newDirectives = viewInjector.getByKey(DIRECTIVE_MAP_KEY); + var viewFuture = viewDef.templateHtml != null ? + new Future.value(_viewCache.fromHtml(viewDef.templateHtml, newDirectives)) : + _viewCache.fromUrl(viewDef.template, newDirectives, Uri.base); + viewFuture.then((ViewFactory viewFactory) { + _cleanUp(); +<<<<<<< HEAD + _childScope = _scope.createProtoChild(); +======= + _childScope = _scope.createChild(_scope.context); +>>>>>>> feat(scope): component is the new context + _view = viewFactory(_childScope, _dirInjector); + _view.nodes.forEach((el) => _element.append(el)); + }); + } + + void _cleanUp() { + if (_view == null) return; + + _view.nodes.forEach((node) => node.remove()); + _childScope.destroy(); + _childScope = null; + _view = null; + } + + Route get route => _viewRoute; + + String get routeName => _viewRoute.name; + + Map get parameters { + var params = new HashMap(); + var route = _viewRoute; + while (route != null) { + params.addAll(route.parameters); + route = route.parent; + } + return params; + } + /** + * Creates a child injector that allows loading new directives, formatters and + * services from the provided modules. + */ + static Injector createChildInjectorWithReload(Injector injector, List modules) { + var modulesToAdd = new List.from(modules); + modulesToAdd.add(new Module() + ..bind(DirectiveMap) + ..bind(FormatterMap)); + + return new ModuleInjector(modulesToAdd, injector); + } +} + + +/** + * Class that can be injected to retrieve information about the current route. + * For example: + * + * @Component(/* ... */) + * class MyComponent implement DetachAware { + * RouteHandle route; + * + * MyComponent(RouteProvider routeProvider) { + * _loadFoo(routeProvider.parameters['fooId']); + * route = routeProvider.route.newHandle(); + * route.onEnter.listen((RouteEvent e) { + * // Do something when the route is activated. + * }); + * route.onPreLeave.listen((RouteEvent e) { + * // Do something when the route is de-activated. + * e.allowLeave(allDataSaved()); + * }); + * } + * + * detach() { + * // The route handle must be discarded. + * route.discard(); + * } + * + * Future allDataSaved() { + * // Check that all data is saved and confirm with the user if needed. + * } + * } + * + * If user component is used outside of `ng-view` directive then + * injected [RouteProvider] will be null. + */ +abstract class RouteProvider { + /** + * Returns [Route] for current view. + */ + Route get route; + + /** + * Returns the name of the current route. + */ + String get routeName; + + /** + * Returns parameters for this route. + */ + Map get parameters; +} diff --git a/test/angular_spec.dart b/test/angular_spec.dart index bd84d6dec..7e88a19c1 100644 --- a/test/angular_spec.dart +++ b/test/angular_spec.dart @@ -247,6 +247,7 @@ main() { "angular.tracing.traceEnter1", "angular.tracing.traceLeave", "angular.tracing.traceLeaveVal", + "angular.watch_group.ContextLocals", "angular.watch_group.PrototypeMap", "angular.watch_group.ReactionFn", "angular.watch_group.Watch", diff --git a/test/change_detection/context_locals_spec.dart b/test/change_detection/context_locals_spec.dart new file mode 100644 index 000000000..84780907a --- /dev/null +++ b/test/change_detection/context_locals_spec.dart @@ -0,0 +1,50 @@ +library context_locals_spec; + +import '../_specs.dart'; +import 'package:angular/change_detection/watch_group.dart'; + +class RootContext { + String a, b, c; + RootContext(this.a, this.b, this.c); +} + +void main() { + describe('Context Locals', () { + RootContext rootCtx; + beforeEach(() { + rootCtx = new RootContext('#a', '#b', '#c'); + }); + + it('should allow retrieving the parent context', () { + var localCtx = new ContextLocals(rootCtx); + expect(localCtx.parentContext).toBe(rootCtx); + }); + + it('should allow testing for supported locals', () { + var localCtx = new ContextLocals(rootCtx, {'foo': 'bar'}); + expect(localCtx.hasProperty('foo')).toBeTruthy(); + expect(localCtx.hasProperty('far')).toBeFalsy(); + expect(localCtx['foo']).toBe('bar'); + }); + + it('should not allow modifying the root context', () { + var localCtx = new ContextLocals(rootCtx, {'a': '@a'}); + expect(localCtx['a']).toBe('@a'); + localCtx['a'] = '@foo'; + expect(localCtx['a']).toBe('@foo'); + expect(rootCtx.a).toBe('#a'); + }); + + it('should write to the local context', () { + var localCtx = new ContextLocals(rootCtx, {'a': 0}); + var childCtx = new ContextLocals(localCtx); + expect(childCtx.hasProperty('a')).toBeFalsy(); + childCtx['a'] = '@a'; + childCtx['b'] = '@b'; + expect(localCtx['a']).toBe(0); + expect(childCtx['a']).toBe('@a'); + expect(childCtx['b']).toBe('@b'); + expect(localCtx.hasProperty('b')).toBeFalsy(); + }); + }); +} \ No newline at end of file diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index 8ac7a9fbd..d9260d150 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -65,7 +65,7 @@ void main() { } beforeEach((Logger _logger) { - context = {}; + context = new ContextLocals({}); var getterFactory = new DynamicFieldGetterFactory(); changeDetector = new DirtyCheckingChangeDetector(getterFactory); watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); @@ -73,6 +73,7 @@ void main() { }); it('should have a toString for debugging', () { + context['a'] = 0; watchGrp.watch(parse('a'), (v, p) {}); watchGrp.newGroup({}); expect("$watchGrp").toEqual( @@ -848,11 +849,13 @@ void main() { }); describe('child group', () { - it('should remove all field watches in group and group\'s children', () { + // todo (vicb) + xit('should remove all field watches in group and group\'s children', () { + context = {'a': null}; watchGrp.watch(parse('a'), (v, p) => logger('0a')); - var child1a = watchGrp.newGroup(new PrototypeMap(context)); - var child1b = watchGrp.newGroup(new PrototypeMap(context)); - var child2 = child1a.newGroup(new PrototypeMap(context)); + var child1a = watchGrp.newGroup(new ContextLocals(context)); + var child1b = watchGrp.newGroup(new ContextLocals(context)); + var child2 = child1a.newGroup(new ContextLocals(context)); child1a.watch(parse('a'), (v, p) => logger('1a')); child1b.watch(parse('a'), (v, p) => logger('1b')); watchGrp.watch(parse('a'), (v, p) => logger('0A')); @@ -881,15 +884,16 @@ void main() { }); it('should remove all method watches in group and group\'s children', () { - context['my'] = new MyClass(logger); + var myClass = new MyClass(logger); + context['my'] = myClass; AST countMethod = new MethodAST(parse('my'), 'count', []); watchGrp.watch(countMethod, (v, p) => logger('0a')); expectOrder(['0a']); - var child1a = watchGrp.newGroup(new PrototypeMap(context)); - var child1b = watchGrp.newGroup(new PrototypeMap(context)); - var child2 = child1a.newGroup(new PrototypeMap(context)); - var child3 = child2.newGroup(new PrototypeMap(context)); + var child1a = watchGrp.newGroup({'my': myClass}); + var child1b = watchGrp.newGroup({'my': myClass}); + var child2 = child1a.newGroup({'my': myClass}); + var child3 = child2.newGroup({'my': myClass}); child1a.watch(countMethod, (v, p) => logger('1a')); expectOrder(['0a', '1a']); child1b.watch(countMethod, (v, p) => logger('1b')); @@ -913,10 +917,11 @@ void main() { }); it('should add watches within its own group', () { - context['my'] = new MyClass(logger); + var myClass = new MyClass(logger); + context['my'] = myClass; AST countMethod = new MethodAST(parse('my'), 'count', []); var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); - var child = watchGrp.newGroup(new PrototypeMap(context)); + var child = watchGrp.newGroup({'my': myClass}); var cb = child.watch(countMethod, (v, p) => logger('b')); expectOrder(['a', 'b']); @@ -958,7 +963,7 @@ void main() { it('should watch children', () { - var childContext = new PrototypeMap(context); + var childContext = new ContextLocals(context); context['a'] = 'OK'; context['b'] = 'BAD'; childContext['b'] = 'OK'; diff --git a/test/core/annotation_src_spec.dart b/test/core/annotation_src_spec.dart index 28aa3e44c..fbb9f3614 100644 --- a/test/core/annotation_src_spec.dart +++ b/test/core/annotation_src_spec.dart @@ -37,15 +37,15 @@ void main() => describe('annotations', () { template: '', templateUrl: '', cssUrl: [''], - publishAs: '', - module: (i){}, + module: (i) {}, map: {}, selector: '', visibility: Directive.LOCAL_VISIBILITY, exportExpressions: [], exportExpressionAttrs: [], useShadowDom: true, - updateBoundElementPropertiesOnEvents: [] + updateBoundElementPropertiesOnEvents: [], + publishAs: '' ); // Check that no fields are null diff --git a/test/core/core_directive_spec.dart b/test/core/core_directive_spec.dart index 6b1199d08..281683f4d 100644 --- a/test/core/core_directive_spec.dart +++ b/test/core/core_directive_spec.dart @@ -23,7 +23,6 @@ void main() { expect(annotation.template).toEqual('template'); expect(annotation.templateUrl).toEqual('templateUrl'); expect(annotation.cssUrls).toEqual(['cssUrls']); - expect(annotation.publishAs).toEqual('ctrl'); expect(annotation.map).toEqual({ 'foo': '=>foo', 'attr': '@attr', @@ -104,7 +103,6 @@ class NullParser implements Parser { template: 'template', templateUrl: 'templateUrl', cssUrl: const ['cssUrls'], - publishAs: 'ctrl', module: AnnotatedIoComponent.module, visibility: Directive.LOCAL_VISIBILITY, exportExpressions: const ['exportExpressions'], diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index c2ee3c4b7..0242c8461 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -711,7 +711,7 @@ main() { context['a'] = {'b': 1}; context['this'] = context; var locals = {'b': 2}; - var fn = parser("this['a'].b").bind(context, ScopeLocals.wrapper); + var fn = parser("this['a'].b").bind(context, ContextLocals.wrapper); expect(fn(locals)).toEqual(1); }); @@ -958,25 +958,26 @@ main() { }); }); - describe('locals', () { + // todo (vicb) it('should expose local variables', () { - expect(parser('a').bind({'a': 6}, ScopeLocals.wrapper)({'a': 1})).toEqual(1); - expect(parser('add(a,b)'). - bind({'b': 1, 'add': (a, b) { return a + b; }}, ScopeLocals.wrapper)({'a': 2})).toEqual(3); + expect(parser('a').bind({'a': 6}, ContextLocals.wrapper)({'a': 1})).toEqual(1); + + expect(parser('add(a,b)') + .bind(new Context(), ContextLocals.wrapper)({'a': 2})).toEqual(3); }); it('should expose traverse locals', () { - expect(parser('a.b').bind({'a': {'b': 6}}, ScopeLocals.wrapper)({'a': {'b':1}})).toEqual(1); - expect(parser('a.b').bind({'a': null}, ScopeLocals.wrapper)({'a': {'b':1}})).toEqual(1); - expect(parser('a.b').bind({'a': {'b': 5}}, ScopeLocals.wrapper)({'a': null})).toEqual(null); + expect(parser('a.b').bind({'a': {'b': 6}}, ContextLocals.wrapper)({'a': {'b':1}})).toEqual(1); + expect(parser('a.b').bind({'a': null}, ContextLocals.wrapper)({'a': {'b':1}})).toEqual(1); + expect(parser('a.b').bind({'a': {'b': 5}}, ContextLocals.wrapper)({'a': null})).toEqual(null); }); it('should work with scopes', (Scope scope) { scope.context['a'] = {'b': 6}; - expect(parser('a.b').bind(scope.context, ScopeLocals.wrapper)({'a': {'b':1}})).toEqual(1); + expect(parser('a.b').bind(scope.context, ContextLocals.wrapper)({'a': {'b':1}})).toEqual(1); }); it('should expose assignment function', () { @@ -984,7 +985,7 @@ main() { expect(fn.assign).toBeNotNull(); var scope = {}; var locals = {"a": {}}; - fn.bind(scope, ScopeLocals.wrapper).assign(123, locals); + fn.bind(scope, ContextLocals.wrapper).assign(123, locals); expect(scope).toEqual({}); expect(locals["a"]).toEqual({'b':123}); }); @@ -1216,3 +1217,8 @@ class HelloFormatter { return 'Hello, $str!'; } } + +class Context { + var b = 1; + add(a, b) => a + b; +} diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 4239562c6..f7ae199b5 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -411,7 +411,7 @@ void main() { var random = new Random(); for (var i = 0; i < 1000; i++) { if (i % 10 == 0) { - scopes = [root.createChild(null)]; + scopes = [root.createChild({})]; listeners = []; steps = []; } @@ -420,9 +420,9 @@ void main() { if (scopes.length > 10) break; var index = random.nextInt(scopes.length); Scope scope = scopes[index]; - var child = scope.createChild(null); + var child = scope.createChild({}); scopes.add(child); - steps.add('scopes[$index].createChild(null)'); + steps.add('scopes[$index].createChild({})'); break; case 1: var index = random.nextInt(scopes.length); @@ -971,37 +971,6 @@ void main() { }); - - describe('ScopeLocals', () { - it('should read from locals', (RootScope scope) { - scope.context['a'] = 'XXX'; - scope.context['c'] = 'C'; - var scopeLocal = new ScopeLocals(scope.context, {'a': 'A', 'b': 'B'}); - expect(scopeLocal['a']).toEqual('A'); - expect(scopeLocal['b']).toEqual('B'); - expect(scopeLocal['c']).toEqual('C'); - }); - - it('should write to Scope', (RootScope scope) { - scope.context['a'] = 'XXX'; - scope.context['c'] = 'C'; - var scopeLocal = new ScopeLocals(scope.context, {'a': 'A', 'b': 'B'}); - - scopeLocal['a'] = 'aW'; - scopeLocal['b'] = 'bW'; - scopeLocal['c'] = 'cW'; - - expect(scope.context['a']).toEqual('aW'); - expect(scope.context['b']).toEqual('bW'); - expect(scope.context['c']).toEqual('cW'); - - expect(scopeLocal['a']).toEqual('A'); - expect(scopeLocal['b']).toEqual('B'); - expect(scopeLocal['c']).toEqual('cW'); - }); - }); - - describe(r'watch/digest', () { it(r'should watch and fire on simple property change', (RootScope rootScope) { var log; diff --git a/test/core/scope_spec.dart.orig b/test/core/scope_spec.dart.orig new file mode 100644 index 000000000..00a22464d --- /dev/null +++ b/test/core/scope_spec.dart.orig @@ -0,0 +1,1775 @@ +library scope2_spec; + +import '../_specs.dart'; +import 'package:angular/change_detection/change_detection.dart' hide ExceptionHandler; +import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'dart:async'; +import 'dart:math'; + +void main() { + describe('scope', () { + beforeEachModule((Module module) { + Map context = {}; + module + ..bind(ChangeDetector, toImplementation: DirtyCheckingChangeDetector) + ..bind(Object, toValue: context) + ..bind(Map, toValue: context) + ..bind(RootScope) + ..bind(_MultiplyFormatter) + ..bind(_ListHeadFormatter) + ..bind(_ListTailFormatter) + ..bind(_SortFormatter) + ..bind(_IdentityFormatter) + ..bind(_MapKeys) + ..bind(ScopeStatsEmitter, toImplementation: MockScopeStatsEmitter); + }); + + describe('AST Bridge', () { + it('should watch field', (Logger logger, Map context, RootScope rootScope) { + context['field'] = 'Worked!'; + rootScope.watch('field', (value, previous) => logger([value, previous])); + expect(logger).toEqual([]); + rootScope.digest(); + expect(logger).toEqual([['Worked!', null]]); + rootScope.digest(); + expect(logger).toEqual([['Worked!', null]]); + }); + + it('should watch field path', (Logger logger, Map context, RootScope rootScope) { + context['a'] = {'b': 'AB'}; + rootScope.watch('a.b', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual(['AB']); + context['a']['b'] = '123'; + rootScope.digest(); + expect(logger).toEqual(['AB', '123']); + context['a'] = {'b': 'XYZ'}; + rootScope.digest(); + expect(logger).toEqual(['AB', '123', 'XYZ']); + }); + + it('should watch math operations', (Logger logger, Map context, RootScope rootScope) { + context['a'] = 1; + context['b'] = 2; + rootScope.watch('a + b + 1', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([4]); + context['a'] = 3; + rootScope.digest(); + expect(logger).toEqual([4, 6]); + context['b'] = 5; + rootScope.digest(); + expect(logger).toEqual([4, 6, 9]); + }); + + + it('should watch literals', (Logger logger, Map context, RootScope rootScope) { + context['a'] = 1; + rootScope + ..watch('', (value, previous) => logger(value)) + ..watch('""', (value, previous) => logger(value)) + ..watch('1', (value, previous) => logger(value)) + ..watch('"str"', (value, previous) => logger(value)) + ..watch('[a, 2, 3]', (value, previous) => logger(value)) + ..watch('{a:a, b:2}', (value, previous) => logger(value)) + ..digest(); + expect(logger).toEqual(['', '', 1, 'str', [1, 2, 3], {'a': 1, 'b': 2}]); + logger.clear(); + context['a'] = 3; + rootScope.digest(); + expect(logger).toEqual([[3, 2, 3], {'a': 3, 'b': 2}]); + }); + + it('should watch nulls', (Logger logger, Map context, RootScope rootScope) { + var r = (value, _) => logger(value); + rootScope + ..watch('null < 0',r) + ..watch('null * 3', r) + ..watch('null + 6', r) + ..watch('5 + null', r) + ..watch('null - 4', r) + ..watch('3 - null', r) + ..watch('null + null', r) + ..watch('null - null', r) + ..watch('null == null', r) + ..watch('null != null', r) + ..digest(); + expect(logger).toEqual([null, null, 6, 5, -4, 3, 0, 0, true, false]); + }); + + it('should invoke closures', (Logger logger, Map context, RootScope rootScope) { + context['fn'] = () { + logger('fn'); + return 1; + }; + context['a'] = {'fn': () { + logger('a.fn'); + return 2; + }}; + rootScope.watch('fn()', (value, previous) => logger('=> $value')); + rootScope.watch('a.fn()', (value, previous) => logger('-> $value')); + rootScope.digest(); + expect(logger).toEqual(['fn', 'a.fn', '=> 1', '-> 2', + /* second loop*/ 'fn', 'a.fn']); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual(['fn', 'a.fn']); + }); + + it('should perform conditionals', (Logger logger, Map context, RootScope rootScope) { + context['a'] = 1; + context['b'] = 2; + context['c'] = 3; + rootScope.watch('a?b:c', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([2]); + logger.clear(); + context['a'] = 0; + rootScope.digest(); + expect(logger).toEqual([3]); + }); + + + xit('should call function', (Logger logger, Map context, RootScope rootScope) { + context['a'] = () { + return () { return 123; }; + }; + rootScope.watch('a()()', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([123]); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual([]); + }); + + it('should access bracket', (Logger logger, Map context, RootScope rootScope) { + context['a'] = {'b': 123}; + rootScope.watch('a["b"]', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([123]); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual([]); + logger.clear(); + + context['a']['b'] = 234; + rootScope.digest(); + expect(logger).toEqual([234]); + }); + + + it('should prefix', (Logger logger, Map context, RootScope rootScope) { + context['a'] = true; + rootScope.watch('!a', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([false]); + logger.clear(); + context['a'] = false; + rootScope.digest(); + expect(logger).toEqual([true]); + }); + + it('should support formatters', (Logger logger, Map context, + RootScope rootScope, FormatterMap formatters) { + context['a'] = 123; + context['b'] = 2; + rootScope.watch('a | multiply:b', (value, previous) => logger(value), + formatters: formatters); + rootScope.digest(); + expect(logger).toEqual([246]); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual([]); + logger.clear(); + }); + + it('should support arrays in formatters', (Logger logger, Map context, + RootScope rootScope, FormatterMap formatters) { + context['a'] = [1]; + rootScope.watch('a | sort | listHead:"A" | listTail:"B"', + (value, previous) => logger(value), formatters: formatters); + rootScope.digest(); + expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 'B']]); + logger.clear(); + + rootScope.digest(); + expect(logger).toEqual([]); + logger.clear(); + + context['a'].add(2); + rootScope.digest(); + expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 2, 'B']]); + logger.clear(); + + // We change the order, but sort should change it to same one and it should not + // call subsequent formatters. + context['a'] = [2, 1]; + rootScope.digest(); + expect(logger).toEqual(['sort']); + logger.clear(); + }); + + it('should support maps in formatters', (Logger logger, Map context, + RootScope rootScope, FormatterMap formatters) { + context['a'] = {'foo': 'bar'}; + rootScope.watch('a | identity | keys', + (value, previous) => logger(value), formatters: formatters); + rootScope.digest(); + expect(logger).toEqual(['identity', 'keys', ['foo']]); + logger.clear(); + + rootScope.digest(); + expect(logger).toEqual([]); + logger.clear(); + + context['a']['bar'] = 'baz'; + rootScope.digest(); + expect(logger).toEqual(['identity', 'keys', ['foo', 'bar']]); + logger.clear(); + }); + + }); + + + describe('properties', () { + describe('root', () { + it('should point to itself', (RootScope rootScope) { + expect(rootScope.rootScope).toEqual(rootScope); + }); + + it('children should point to root', (RootScope rootScope) { +<<<<<<< HEAD + var child = rootScope.createProtoChild(); + expect(child.rootScope).toEqual(rootScope); + expect(child.createProtoChild().rootScope).toEqual(rootScope); +======= + var child = rootScope.createChild(rootScope.context); + expect(child.rootScope).toEqual(rootScope); + expect(child.createChild(rootScope.context).rootScope).toEqual(rootScope); +>>>>>>> feat(scope): component is the new context + }); + }); + + + describe('parent', () { + it('should not have parent', (RootScope rootScope) { + expect(rootScope.parentScope).toEqual(null); + expect(rootScope.id).toEqual(''); + }); + + + it('should point to parent', (RootScope rootScope) { +<<<<<<< HEAD + var child = rootScope.createProtoChild(); + expect(child.id).toEqual(':0'); + expect(rootScope.parentScope).toEqual(null); + expect(child.parentScope).toEqual(rootScope); + expect(child.createProtoChild().parentScope).toEqual(child); +======= + var child = rootScope.createChild(rootScope.context); + expect(child.id).toEqual(':0'); + expect(rootScope.parentScope).toEqual(null); + expect(child.parentScope).toEqual(rootScope); + expect(child.createChild(rootScope.context).parentScope).toEqual(child); +>>>>>>> feat(scope): component is the new context + }); + }); + }); + + + describe(r'events', () { + + describe('on', () { + it('should allow emit/broadcast when no listeners', (RootScope scope) { + scope.emit('foo'); + scope.broadcast('foo'); + }); + + + it(r'should add listener for both emit and broadcast events', (RootScope rootScope) { + var log = '', +<<<<<<< HEAD + child = rootScope.createProtoChild(); +======= + child = rootScope.createChild(rootScope.context); +>>>>>>> feat(scope): component is the new context + + eventFn(event) { + expect(event).not.toEqual(null); + log += 'X'; + } + + child.on('abc').listen(eventFn); + expect(log).toEqual(''); + + child.emit('abc'); + expect(log).toEqual('X'); + + child.broadcast('abc'); + expect(log).toEqual('XX'); + }); + + + it(r'should return a function that deregisters the listener', (RootScope rootScope) { + var log = ''; +<<<<<<< HEAD + var child = rootScope.createProtoChild(); +======= + var child = rootScope.createChild(rootScope.context); +>>>>>>> feat(scope): component is the new context + var subscription; + + eventFn(e) { + log += 'X'; + } + + subscription = child.on('abc').listen(eventFn); + expect(log).toEqual(''); + expect(subscription).toBeDefined(); + + child.emit(r'abc'); + child.broadcast('abc'); + expect(log).toEqual('XX'); + + log = ''; + expect(subscription.cancel()).toBe(null); + child.emit(r'abc'); + child.broadcast('abc'); + expect(log).toEqual(''); + }); + + it('should not trigger assertions on scope fork', (RootScope root) { + var d1 = root.createChild({}); + var d2 = root.createChild({}); + var d3 = d2.createChild({}); + expect(root.apply).not.toThrow(); + d1.on(ScopeEvent.DESTROY).listen((_) => null); + expect(root.apply).not.toThrow(); + d3.on(ScopeEvent.DESTROY).listen((_) => null); + expect(root.apply).not.toThrow(); + d2.on(ScopeEvent.DESTROY).listen((_) => null); + expect(root.apply).not.toThrow(); + }); + + it('should not too eagerly create own streams', (RootScope root) { + var a = root.createChild({}); + var a2 = root.createChild({}); + var b = a.createChild({}); + var c = b.createChild({}); + var d = c.createChild({}); + var e = d.createChild({}); + + getStreamState() => [root.hasOwnStreams, a.hasOwnStreams, a2.hasOwnStreams, + b.hasOwnStreams, c.hasOwnStreams, d.hasOwnStreams, + e.hasOwnStreams]; + + expect(getStreamState()).toEqual([false, false, false, false, false, false, false]); + expect(root.apply).not.toThrow(); + + e.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([false, false, false, false, false, false, true]); + expect(root.apply).not.toThrow(); + + d.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([false, false, false, false, false, true, true]); + expect(root.apply).not.toThrow(); + + b.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([false, false, false, true, false, true, true]); + expect(root.apply).not.toThrow(); + + c.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([false, false, false, true, true, true, true]); + expect(root.apply).not.toThrow(); + + a.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([false, true, false, true, true, true, true]); + expect(root.apply).not.toThrow(); + + a2.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([true, true, true, true, true, true, true]); + expect(root.apply).not.toThrow(); + }); + + + it('should not properly merge streams', (RootScope root) { + var a = root.createChild({}); + var a2 = root.createChild({}); + var b = a.createChild({}); + var c = b.createChild({}); + var d = c.createChild({}); + var e = d.createChild({}); + + getStreamState() => [root.hasOwnStreams, a.hasOwnStreams, a2.hasOwnStreams, + b.hasOwnStreams, c.hasOwnStreams, d.hasOwnStreams, + e.hasOwnStreams]; + + expect(getStreamState()).toEqual([false, false, false, false, false, false, false]); + expect(root.apply).not.toThrow(); + + a2.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([false, false, true, false, false, false, false]); + expect(root.apply).not.toThrow(); + + e.on(ScopeEvent.DESTROY).listen((_) => null); + expect(getStreamState()).toEqual([true, false, true, false, false, false, true]); + expect(root.apply).not.toThrow(); + }); + + + it('should clean up on cancel', (RootScope root) { + var child = root.createChild(null); + var cl = child.on("E").listen((e) => null); + var rl = root.on("E").listen((e) => null); + rl.cancel(); + expect(root.apply).not.toThrow(); + }); + + + it('should find random bugs', (RootScope root) { + List scopes; + List listeners; + List steps; + var random = new Random(); + for (var i = 0; i < 1000; i++) { + if (i % 10 == 0) { + scopes = [root.createChild({})]; + listeners = []; + steps = []; + } + switch(random.nextInt(4)) { + case 0: + if (scopes.length > 10) break; + var index = random.nextInt(scopes.length); + Scope scope = scopes[index]; + var child = scope.createChild({}); + scopes.add(child); + steps.add('scopes[$index].createChild({})'); + break; + case 1: + var index = random.nextInt(scopes.length); + Scope scope = scopes[index]; + listeners.add(scope.on('E').listen((e) => null)); + steps.add('scopes[$index].on("E").listen((e)=>null)'); + break; + case 2: + if (scopes.length < 3) break; + var index = random.nextInt(scopes.length - 1) + 1; + Scope scope = scopes[index]; + scope.destroy(); + scopes = scopes.where((Scope s) => s.isAttached).toList(); + steps.add('scopes[$index].destroy()'); + break; + case 3: + if (listeners.length == 0) break; + var index = random.nextInt(listeners.length); + var l = listeners[index]; + l.cancel(); + listeners.remove(l); + steps.add('listeners[$index].cancel()'); + break; + } + try { + root.apply(); + } catch (e) { + expect('').toEqual(steps.join(';\n')); + } + } + }); + }); + + + describe('emit', () { + var log, child, grandChild, greatGrandChild; + + logger(event) { + log.add(event.currentScope.context['id']); + } + + beforeEachModule(() { + return (RootScope rootScope) { + log = []; + child = rootScope.createChild({'id': 1}); + grandChild = child.createChild({'id': 2}); + greatGrandChild = grandChild.createChild({'id': 3}); + + rootScope.context['id'] = 0; + + rootScope.on('myEvent').listen(logger); + child.on('myEvent').listen(logger); + grandChild.on('myEvent').listen(logger); + greatGrandChild.on('myEvent').listen(logger); + }; + }); + + it(r'should bubble event up to the root scope', (RootScope rootScope) { + grandChild.emit(r'myEvent'); + expect(log.join('>')).toEqual('2>1>0'); + }); + + + describe('exceptions', () { + beforeEachModule((Module module) { + module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler); + }); + + + it(r'should dispatch exceptions to the exceptionHandler', (ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + child.on('myEvent').listen((e) { throw 'bubbleException'; }); + grandChild.emit(r'myEvent'); + expect(log.join('>')).toEqual('2>1>0'); + expect(exceptionHandler.errors[0].error).toEqual('bubbleException'); + }); + + + it('should throw "model unstable" error when observer is present', (RootScope rootScope, VmTurnZone zone, ExceptionHandler e) { + // Generates a different, equal, list on each evaluation. + rootScope.context['list'] = new UnstableList(); + + rootScope.watch('list.list', (n, v) => null, canChangeModel: true); + try { + zone.run(() => null); + } catch(_) {} + + var errors = (e as LoggingExceptionHandler).errors; + expect(errors.length).toEqual(1); + expect(errors.first.error, startsWith('Model did not stabilize')); + }); + }); + + it(r'should allow stopping event propagation', (RootScope rootScope) { + child.on('myEvent').listen((event) { event.stopPropagation(); }); + grandChild.emit(r'myEvent'); + expect(log.join('>')).toEqual('2>1'); + }); + + + it(r'should forward method arguments', (RootScope rootScope) { + var eventName; + var eventData; + child.on('abc').listen((event) { + eventName = event.name; + eventData = event.data; + }); + child.emit('abc', ['arg1', 'arg2']); + expect(eventName).toEqual('abc'); + expect(eventData).toEqual(['arg1', 'arg2']); + }); + + + describe(r'event object', () { + it(r'should have methods/properties', (RootScope rootScope) { + var event; + child.on('myEvent').listen((e) { + expect(e.targetScope).toBe(grandChild); + expect(e.currentScope).toBe(child); + expect(e.name).toBe('myEvent'); + event = e; + }); + grandChild.emit(r'myEvent'); + expect(event).toBeDefined(); + }); + + + it(r'should have preventDefault method and defaultPrevented property', (RootScope rootScope) { + var event = grandChild.emit(r'myEvent'); + expect(event.defaultPrevented).toBe(false); + + child.on('myEvent').listen((event) { + event.preventDefault(); + }); + event = grandChild.emit(r'myEvent'); + expect(event.defaultPrevented).toBe(true); + }); + }); + }); + + + describe('broadcast', () { + describe(r'event propagation', () { + var log, child1, child2, child3, grandChild11, grandChild21, grandChild22, grandChild23, + greatGrandChild211; + + logger(event) { + log.add(event.currentScope.context['id']); + } + + beforeEach((RootScope rootScope) { + log = []; + child1 = rootScope.createChild({}); + child2 = rootScope.createChild({}); + child3 = rootScope.createChild({}); + grandChild11 = child1.createChild({}); + grandChild21 = child2.createChild({}); + grandChild22 = child2.createChild({}); + grandChild23 = child2.createChild({}); + greatGrandChild211 = grandChild21.createChild({}); + + rootScope.context['id'] = 0; + child1.context['id'] = 1; + child2.context['id'] = 2; + child3.context['id'] = 3; + grandChild11.context['id'] = 11; + grandChild21.context['id'] = 21; + grandChild22.context['id'] = 22; + grandChild23.context['id'] = 23; + greatGrandChild211.context['id'] = 211; + + rootScope.on('myEvent').listen(logger); + child1.on('myEvent').listen(logger); + child2.on('myEvent').listen(logger); + child3.on('myEvent').listen(logger); + grandChild11.on('myEvent').listen(logger); + grandChild21.on('myEvent').listen(logger); + grandChild22.on('myEvent').listen(logger); + grandChild23.on('myEvent').listen(logger); + greatGrandChild211.on('myEvent').listen(logger); + + // R + // / | \ + // 1 2 3 + // / / | \ + // 11 21 22 23 + // | + // 211 + }); + + + it(r'should broadcast an event from the root scope', (RootScope rootScope) { + rootScope.broadcast('myEvent'); + expect(log.join('>')).toEqual('0>1>11>2>21>211>22>23>3'); + }); + + + it(r'should broadcast an event from a child scope', (RootScope rootScope) { + child2.broadcast('myEvent'); + expect(log.join('>')).toEqual('2>21>211>22>23'); + }); + + + it(r'should broadcast an event from a leaf scope with a sibling', (RootScope rootScope) { + grandChild22.broadcast('myEvent'); + expect(log.join('>')).toEqual('22'); + }); + + + it(r'should broadcast an event from a leaf scope without a sibling', (RootScope rootScope) { + grandChild23.broadcast('myEvent'); + expect(log.join('>')).toEqual('23'); + }); + + + it(r'should not not fire any listeners for other events', (RootScope rootScope) { + rootScope.broadcast('fooEvent'); + expect(log.join('>')).toEqual(''); + }); + + + it(r'should return event object', (RootScope rootScope) { + var result = child1.broadcast('some'); + + expect(result).toBeDefined(); + expect(result.name).toBe('some'); + expect(result.targetScope).toBe(child1); + }); + + + it('should skip scopes which dont have given event', + (RootScope rootScope, Logger log) { + var child1 = rootScope.createChild('A'); + rootScope.createChild('A1'); + rootScope.createChild('A2'); + rootScope.createChild('A3'); + var child2 = rootScope.createChild('B'); + child2.on('event').listen((e) => log(e.data)); + rootScope.broadcast('event', 'OK'); + expect(log).toEqual(['OK']); + }); + }); + + + describe(r'listener', () { + it(r'should receive event object', (RootScope rootScope) { + var scope = rootScope, + child = scope.createChild({}), + event; + + child.on('fooEvent').listen((e) { + event = e; + }); + scope.broadcast('fooEvent'); + + expect(event.name).toBe('fooEvent'); + expect(event.targetScope).toBe(scope); + expect(event.currentScope).toBe(child); + }); + + it(r'should support passing messages as varargs', (RootScope rootScope) { + var scope = rootScope, + child = scope.createChild({}), + args; + + child.on('fooEvent').listen((e) { + args = e.data; + }); + scope.broadcast('fooEvent', ['do', 're', 'me', 'fa']); + + expect(args.length).toBe(4); + expect(args).toEqual(['do', 're', 'me', 'fa']); + }); + + it('should allow removing/adding listener during an event', (RootScope rootScope, Logger log) { + StreamSubscription subscription; + subscription = rootScope.on('foo').listen((_) { + subscription.cancel(); + rootScope.on('foo').listen((_) => log(3)); + log(2); + }); + expect(() { + log(1); + rootScope.broadcast('foo'); + }).not.toThrow(); + rootScope.broadcast('foo'); + expect(log).toEqual([1, 2, 3]); + }); + }); + }); + }); + + + describe(r'destroy', () { + var first = null, middle = null, last = null, log = null; + + beforeEach((RootScope rootScope) { + log = ''; + + first = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); + middle = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); + last = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); + + first.watch('check(1)', (v, l) {}); + middle.watch('check(2)', (v, l) {}); + last.watch('check(3)', (v, l) {}); + + first.on(ScopeEvent.DESTROY).listen((e) { log += 'destroy:first;'; }); + + rootScope.digest(); + log = ''; + }); + + + it(r'should ignore remove on root', (RootScope rootScope) { + rootScope.destroy(); + rootScope.digest(); + expect(log).toEqual('123'); + }); + + + it(r'should remove first', (RootScope rootScope) { + first.destroy(); + rootScope.digest(); + expect(log).toEqual('destroy:first;23'); + }); + + + it(r'should remove middle', (RootScope rootScope) { + middle.destroy(); + rootScope.digest(); + expect(log).toEqual('13'); + }); + + + it(r'should remove last', (RootScope rootScope) { + last.destroy(); + rootScope.digest(); + expect(log).toEqual('12'); + }); + + + it(r'should broadcast the destroy event', (RootScope rootScope) { + var log = []; + first.on(ScopeEvent.DESTROY).listen((s) => log.add('first')); + var child = first.createChild({}); + child.on(ScopeEvent.DESTROY).listen((s) => log.add('first-child')); + + first.destroy(); + expect(log).toEqual(['first', 'first-child']); + }); + + + it('should not call reaction function on destroyed scope', (RootScope rootScope, Logger log) { + rootScope.context['name'] = 'misko'; + var child = rootScope.createChild(rootScope.context); + rootScope.watch('name', (v, _) { + log('root $v'); + if (v == 'destroy') { + child.destroy(); + } + }); + rootScope.watch('name', (v, _) => log('root2 $v')); + child.watch('name', (v, _) => log('child $v')); + rootScope.apply(); + expect(log).toEqual(['root misko', 'root2 misko', 'child misko']); + log.clear(); + + rootScope.context['name'] = 'destroy'; + rootScope.apply(); + expect(log).toEqual(['root destroy', 'root2 destroy']); + }); + + + it('should not call reaction fn when destroyed', (RootScope scope) { + var testScope = scope.createChild({}); + bool called = false; + testScope.watch('items', (_, __) { + called = true; + }); + testScope.destroy(); + scope.apply(); + expect(called).toBeFalsy(); + }); + }); + + + describe('digest lifecycle', () { + it(r'should apply expression with full lifecycle', (RootScope rootScope) { + var log = ''; + var child = rootScope.createChild({"parent": rootScope.context}); + rootScope.watch('a', (a, _) { log += '1'; }); + child.apply('parent.a = 0'); + expect(log).toEqual('1'); + }); + + describe(r'exceptions', () { + var log; + beforeEachModule((Module module) { + return module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler); + }); + + beforeEach((RootScope rootScope) { + rootScope.context['log'] = () { log += 'digest;'; return null; }; + log = ''; + rootScope.watch('log()', (v, o) => null); + rootScope.digest(); + log = ''; + }); + + it(r'should catch exceptions', (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + var log = []; + var child = rootScope.createChild({}); + rootScope.watch('a', (a, _) => log.add('1')); + rootScope.context['a'] = 0; + child.apply(() { throw 'MyError'; }); + expect(log.join(',')).toEqual('1'); + expect(exceptionHandler.errors[0].error).toEqual('MyError'); + exceptionHandler.errors.removeAt(0); + exceptionHandler.assertEmpty(); + }); + + + it(r'should execute and return value and update', + (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + rootScope.context['name'] = 'abc'; + expect(rootScope.apply((context) => context['name'])).toEqual('abc'); + expect(log).toEqual('digest;digest;'); + exceptionHandler.assertEmpty(); + }); + + + it(r'should execute and return value and update', (RootScope rootScope) { + rootScope.context['name'] = 'abc'; + expect(rootScope.apply('name', {'name': 123})).toEqual(123); + }); + + + it(r'should catch exception and update', (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + var error = 'MyError'; + rootScope.apply(() { throw error; }); + expect(log).toEqual('digest;digest;'); + expect(exceptionHandler.errors[0].error).toEqual(error); + }); + }); + + it(r'should properly reset phase on exception', (RootScope rootScope) { + var error = 'MyError'; + expect(() => rootScope.apply(() { throw error; })).toThrowWith(message: error); + expect(() => rootScope.apply(() { throw error; })).toThrowWith(message: error); + }); + }); + + + describe('flush lifecycle', () { + it(r'should apply expression with full lifecycle', (RootScope rootScope) { + var log = ''; + var child = rootScope.createChild({"parent": rootScope.context}); + rootScope.watch('a', (a, _) { log += '1'; }, canChangeModel: false); + child.apply('parent.a = 0'); + expect(log).toEqual('1'); + }); + + + it(r'should schedule domWrites and domReads', (RootScope rootScope) { + var log = ''; + var child = rootScope.createChild({"parent": rootScope.context}); + rootScope.watch('a', (a, _) { log += '1'; }, canChangeModel: false); + child.apply('parent.a = 0'); + expect(log).toEqual('1'); + }); + + describe(r'exceptions', () { + var log; + beforeEachModule((Module module) { + return module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler); + }); + beforeEach((RootScope rootScope) { + rootScope.context['log'] = () { log += 'digest;'; return null; }; + log = ''; + rootScope.watch('log()', (v, o) => null, canChangeModel: false); + rootScope.digest(); + log = ''; + }); + + it(r'should catch exceptions', (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + var log = []; + var child = rootScope.createChild({}); + rootScope.watch('a', (a, _) => log.add('1'), canChangeModel: false); + rootScope.context['a'] = 0; + child.apply(() { throw 'MyError'; }); + expect(log.join(',')).toEqual('1'); + expect(exceptionHandler.errors[0].error).toEqual('MyError'); + exceptionHandler.errors.removeAt(0); + exceptionHandler.assertEmpty(); + }); + + it(r'should execute and return value and update', + (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + rootScope.context['name'] = 'abc'; + expect(rootScope.apply((context) => context['name'])).toEqual('abc'); + expect(log).toEqual('digest;digest;'); + exceptionHandler.assertEmpty(); + }); + + it(r'should execute and return value and update', (RootScope rootScope) { + rootScope.context['name'] = 'abc'; + expect(rootScope.apply('name', {'name': 123})).toEqual(123); + }); + + it(r'should catch exception and update', (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + var error = 'MyError'; + rootScope.apply(() { throw error; }); + expect(log).toEqual('digest;digest;'); + expect(exceptionHandler.errors[0].error).toEqual(error); + }); + + it(r'should throw assertion when model changes in flush', (RootScope rootScope, Logger log) { + var retValue = 1; + rootScope.context['logger'] = (name) { log(name); return retValue; }; + + rootScope.watch('logger("watch")', (n, v) => null); + rootScope.watch('logger("flush")', (n, v) => null, + canChangeModel: false); + + // clear watches + rootScope.digest(); + log.clear(); + + rootScope.flush(); + expect(log).toEqual(['flush', /*assertion*/ 'watch', 'flush']); + + retValue = 2; + expect(rootScope.flush). + toThrowWith(message: 'Observer reaction functions should not change model. \n' + 'These watch changes were detected: logger("watch"): 2 <= 1\n' + 'These observe changes were detected: '); + }); + }); + + }); + + describe(r'watch/digest', () { + it(r'should watch and fire on simple property change', (RootScope rootScope) { + var log; + + rootScope.watch('name', (a, b) { + log = [a, b]; + }); + rootScope.digest(); + log = null; + + expect(log).toEqual(null); + rootScope.digest(); + expect(log).toEqual(null); + rootScope.context['name'] = 'misko'; + rootScope.digest(); + expect(log).toEqual(['misko', null]); + }); + + + it('should watch/observe on objects other then context (DEPRECATED)', (RootScope rootScope) { + var log = ''; + var map = {'a': 'A', 'b': 'B'}; + rootScope.watch('a', (a, b) => log += a, context: map); + rootScope.watch('b', (a, b) => log += a, context: map); + rootScope.apply(); + expect(log).toEqual('AB'); + }); + + + it(r'should watch and fire on expression change', (RootScope rootScope) { + var log; + + rootScope.watch('name.first', (a, b) => log = [a, b]); + rootScope.digest(); + log = null; + + rootScope.context['name'] = {}; + expect(log).toEqual(null); + rootScope.digest(); + expect(log).toEqual(null); + rootScope.context['name']['first'] = 'misko'; + rootScope.digest(); + expect(log).toEqual(['misko', null]); + }); + + + describe('exceptions', () { + beforeEachModule((Module module) { + module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler); + }); + it(r'should delegate exceptions', (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + rootScope.watch('a', (n, o) {throw 'abc';}); + rootScope.context['a'] = 1; + rootScope.digest(); + expect(exceptionHandler.errors.length).toEqual(1); + expect(exceptionHandler.errors[0].error).toEqual('abc'); + }); + }); + + + + it(r'should fire watches in order of addition', (RootScope rootScope) { + // this is not an external guarantee, just our own sanity + var log = ''; + rootScope + ..watch('a', (a, b) { log += 'a'; }) + ..watch('b', (a, b) { log += 'b'; }) + ..watch('c', (a, b) { log += 'c'; }) + ..context['a'] = rootScope.context['b'] = rootScope.context['c'] = 1 + ..digest(); + expect(log).toEqual('abc'); + }); + + + it(r'should call child watchers in addition order', (RootScope rootScope) { + // this is not an external guarantee, just our own sanity + var log = ''; + var childA = rootScope.createChild({}); + var childB = rootScope.createChild({}); + var childC = rootScope.createChild({}); + childA.watch('a', (a, b) { log += 'a'; }); + childB.watch('b', (a, b) { log += 'b'; }); + childC.watch('c', (a, b) { log += 'c'; }); + childA.context['a'] = childB.context['b'] = childC.context['c'] = 1; + rootScope.digest(); + expect(log).toEqual('abc'); + }); + + + it(r'should run digest multiple times', (RootScope rootScope) { + // tests a traversal edge case which we originally missed + var log = []; + var childA = rootScope.createChild({'log': log}); + var childB = rootScope.createChild({'log': log}); + + rootScope.context['log'] = log; + + rootScope.watch("log.add('r')", (_, __) => null); + childA.watch("log.add('a')", (_, __) => null); + childB.watch("log.add('b')", (_, __) => null); + + // init + rootScope.digest(); + expect(log.join('')).toEqual('rabrab'); + }); + + + it(r'should repeat watch cycle while model changes are identified', (RootScope rootScope) { + var log = ''; + rootScope + ..watch('c', (v, b) {rootScope.context['d'] = v; log+='c'; }) + ..watch('b', (v, b) {rootScope.context['c'] = v; log+='b'; }) + ..watch('a', (v, b) {rootScope.context['b'] = v; log+='a'; }) + ..digest(); + log = ''; + rootScope.context['a'] = 1; + rootScope.digest(); + expect(rootScope.context['b']).toEqual(1); + expect(rootScope.context['c']).toEqual(1); + expect(rootScope.context['d']).toEqual(1); + expect(log).toEqual('abc'); + }); + + + it(r'should repeat watch cycle from the root element', (RootScope rootScope) { + var log = []; + rootScope.context['log'] = log; + var child = rootScope.createChild({'log':log}); + rootScope.watch("log.add('a')", (_, __) => null); + child.watch("log.add('b')", (_, __) => null); + rootScope.digest(); + expect(log.join('')).toEqual('abab'); + }); + + + it(r'should not fire upon watch registration on initial digest', (RootScope rootScope) { + var log = ''; + rootScope.context['a'] = 1; + rootScope.watch('a', (a, b) { log += 'a'; }); + rootScope.watch('b', (a, b) { log += 'b'; }); + rootScope.digest(); + log = ''; + rootScope.digest(); + expect(log).toEqual(''); + }); + + + it(r'should prevent digest recursion', (RootScope rootScope) { + var callCount = 0; + rootScope.watch('name', (a, b) { + expect(() { + rootScope.digest(); + }).toThrowWith(message: 'digest already in progress'); + callCount++; + }); + rootScope.context['name'] = 'a'; + rootScope.digest(); + expect(callCount).toEqual(1); + }); + + + it(r'should return a function that allows listeners to be unregistered', + (RootScope rootScope) { + var listener = guinness.createSpy('watch listener'); + var watch; + + watch = rootScope.watch('foo', listener); + rootScope.digest(); //init + expect(listener).toHaveBeenCalled(); + expect(watch).toBeDefined(); + + listener.reset(); + rootScope.context['foo'] = 'bar'; + rootScope.digest(); //trigger + expect(listener).toHaveBeenCalledOnce(); + + listener.reset(); + rootScope.context['foo'] = 'baz'; + watch.remove(); + rootScope.digest(); //trigger + expect(listener).not.toHaveBeenCalled(); + }); + + + it(r'should be possible to remove every watch', + (RootScope rootScope, FormatterMap formatters) { + rootScope.context['foo'] = 'bar'; + var watch1 = rootScope.watch('(foo|json)+"bar"', (v, p) => null, + formatters: formatters); + var watch2 = rootScope.watch('(foo|json)+"bar"', (v, p) => null, + formatters: formatters); + + expect(() => watch1.remove()).not.toThrow(); + expect(() => watch2.remove()).not.toThrow(); + }); + + + it(r'should not infinitely digest when current value is NaN', (RootScope rootScope) { + rootScope.context['nan'] = double.NAN; + rootScope.watch('nan', (_, __) => null); + + expect(() { + rootScope.digest(); + }).not.toThrow(); + }); + + + it(r'should prevent infinite digest and should log firing expressions', (RootScope rootScope) { + rootScope.context['a'] = 0; + rootScope.context['b'] = 0; + rootScope.watch('a', (a, __) => rootScope.context['a'] = a + 1); + rootScope.watch('b', (b, __) => rootScope.context['b'] = b + 1); + + expect(() { + rootScope.digest(); + }).toThrowWith(message: 'Model did not stabilize in 10 digests. ' + 'Last 3 iterations:\n' + 'a: 7 <= 6, b: 7 <= 6\n' + 'a: 8 <= 7, b: 8 <= 7\n' + 'a: 9 <= 8, b: 9 <= 8'); + }); + + + it(r'should detect infinite digest through runAsync', (RootScope rootScope) { + rootScope.context['value'] = () { rootScope.runAsync(() {}); return 'a'; }; + rootScope.watch('value()', (_, __) {}); + + expect(() { + rootScope.digest(); + }).toThrowWith(message: 'Model did not stabilize in 10 digests. ' + 'Last 3 iterations:\n' + 'async:1\n' + 'async:1\n' + 'async:1'); + }); + + + it(r'should always call the watchr with newVal and oldVal equal on the first run', + (RootScope rootScope) { + var log = []; + var logger = (newVal, oldVal) { + var val = (newVal == oldVal || (newVal != oldVal && oldVal != newVal)) ? newVal : 'xxx'; + log.add(val); + }; + + rootScope + ..context['nanValue'] = double.NAN + ..context['nullValue'] = null + ..context['emptyString'] = '' + ..context['falseValue'] = false + ..context['numberValue'] = 23 + ..watch('nanValue', logger) + ..watch('nullValue', logger) + ..watch('emptyString', logger) + ..watch('falseValue', logger) + ..watch('numberValue', logger) + ..digest(); + + expect(log.removeAt(0).isNaN).toEqual(true); //guinness's toBe and toEqual don't work well with NaNs + expect(log).toEqual([null, '', false, 23]); + log = []; + rootScope.digest(); + expect(log).toEqual([]); + }); + + + it('should properly watch constants', (RootScope rootScope, Logger log) { + rootScope.watch('[1, 2]', (v, o) => log([v, o])); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([[[1, 2], null]]); + }); + + + it('should properly watch array of fields 1', (RootScope rootScope, Logger log) { + rootScope.context['foo'] = 12; + rootScope.context['bar'] = 34; + rootScope.watch('[foo, bar]', (v, o) => log([v, o])); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([[[12, 34], null]]); + log.clear(); + + rootScope.context['foo'] = 56; + rootScope.context['bar'] = 78; + rootScope.apply(); + expect(log).toEqual([[[56, 78], [12, 34]]]); + }); + + + it('should properly watch array of fields 2', (RootScope rootScope, Logger log) { + rootScope.context['foo'] = () => 12; + rootScope.watch('foo()', (v, o) => log(v)); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([12]); + }); + + + it('should properly watch array of fields 3', (RootScope rootScope, Logger log) { + rootScope.context['foo'] = 'abc'; + rootScope.watch('foo.contains("b")', (v, o) => log([v, o])); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([[true, null]]); + log.clear(); + }); + + it('should watch closures both as a leaf and as method call', (RootScope rootScope, Logger log) { + rootScope.context['foo'] = new Foo(); + rootScope.context['increment'] = null; + rootScope.watch('foo.increment', (v, _) => rootScope.context['increment'] = v); + rootScope.watch('increment(1)', (v, o) => log([v, o])); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([[null, null], [2, null]]); + log.clear(); + }); + + it('should not trigger new watcher in the flush where it was added', (Scope scope) { + var log = [] ; + scope.context['foo'] = () => 'foo'; + scope.context['name'] = 'misko'; + scope.context['list'] = [2, 3]; + scope.context['map'] = {'bar': 'chocolate'}; + scope.watch('1', (value, __) { + expect(value).toEqual(1); + scope.watch('foo()', (value, __) => log.add(value)); + scope.watch('name', (value, __) => log.add(value)); + scope.watch('(foo() + "-" + name).toUpperCase()', (value, __) => log.add(value)); + scope.watch('list', (value, __) => log.add(value)); + scope.watch('map', (value, __) => log.add(value)); + }); + scope.apply(); + expect(log).toEqual(['foo', 'misko', 'FOO-MISKO', [2, 3], {'bar': 'chocolate'}]); + }); + + + it('should allow multiple nested watches', (RootScope scope) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + scope.watch('1', (_, __) { + // make this deeper then ScopeTTL; + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + expect(scope.apply).not.toThrow(); + }); + + + it('should properly watch array of fields 4', (RootScope rootScope, Logger log) { + rootScope.watch('[ctrl.foo, ctrl.bar]', (v, o) => log([v, o])); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([[[null, null], null]]); + log.clear(); + + rootScope.context['ctrl'] = {'foo': 56, 'bar': 78}; + rootScope.apply(); + expect(log).toEqual([[[56, 78], [null, null]]]); + }); + }); + + + describe('special binding modes', () { + it('should bind one time', (RootScope rootScope, Logger log) { + rootScope.watch('foo', (v, _) => log('foo:$v')); + rootScope.watch(':foo', (v, _) => log(':foo:$v')); + rootScope.watch('::foo', (v, _) => log('::foo:$v')); + + rootScope.apply(); + expect(log).toEqual(['foo:null']); + log.clear(); + + rootScope.context['foo'] = true; + rootScope.apply(); + expect(log).toEqual(['foo:true', ':foo:true', '::foo:true']); + log.clear(); + + rootScope.context['foo'] = 123; + rootScope.apply(); + expect(log).toEqual(['foo:123', ':foo:123']); + log.clear(); + + rootScope.context['foo'] = null; + rootScope.apply(); + expect(log).toEqual(['foo:null']); + log.clear(); + }); + }); + + + describe('runAsync', () { + it(r'should run callback before watch', (RootScope rootScope) { + var log = ''; + rootScope.runAsync(() { log += 'parent.async;'; }); + rootScope.watch('value', (_, __) { log += 'parent.digest;'; }); + rootScope.digest(); + expect(log).toEqual('parent.async;parent.digest;'); + }); + + it(r'should cause a digest rerun', (RootScope rootScope) { + rootScope.context['log'] = ''; + rootScope.context['value'] = 0; + // NOTE(deboer): watch listener string functions not yet supported + //rootScope.watch('value', 'log = log + ".";'); + rootScope.watch('value', (_, __) { rootScope.context['log'] += "."; }); + rootScope.watch('init', (_, __) { + rootScope.runAsync(() => rootScope.eval('value = 123; log = log + "=" ')); + expect(rootScope.context['value']).toEqual(0); + }); + rootScope.digest(); + expect(rootScope.context['log']).toEqual('.=.'); + }); + + it(r'should run async in the same order as added', (RootScope rootScope) { + rootScope.context['log'] = ''; + rootScope.runAsync(() => rootScope.eval("log = log + 1")); + rootScope.runAsync(() => rootScope.eval("log = log + 2")); + rootScope.digest(); + expect(rootScope.context['log']).toEqual('12'); + }); + }); + + describe('microtask processing', () { + beforeEach((VmTurnZone zone, RootScope scope, Logger log) { + var onTurnDone = zone.onTurnDone; + zone.onTurnDone = () { + log('['); + onTurnDone(); + log(']'); + }; + var onScheduleMicrotask = zone.onScheduleMicrotask; + zone.onScheduleMicrotask = (fn) { + log('('); + try { + onScheduleMicrotask(fn); + } catch (e) { + log('CATCH: $e'); + } + log(')'); + }; + }); + + it('should schedule apply after future resolution', + async((Logger log, VmTurnZone zone, RootScope scope) { + Completer completer; + zone.run(() { + completer = new Completer(); + completer.future.then((value) { + log('then($value)'); + }); + }); + + scope.runAsync(() => log('before')); + log.clear(); + completer.complete('OK'); // this one causes APPLY which processe 'before' + // This one schedules work but apply already run so it does not execute. + scope.runAsync(() => log('NOT_EXECUTED')); + + expect(log).toEqual(['(', ')', '[', 'before', 'then(OK)', ']']); + }) + ); + + it('should schedule microtask to runAsync queue during digest', + async((Logger log, VmTurnZone zone, RootScope scope) { + Completer completer; + zone.run(() { + completer = new Completer(); + completer.future. + then((value) { + scope.runAsync(() => log('in(${scope.state})')); + return new Future.value(value); + }). + then((value) { + log('then($value)'); + }); + }); + log.clear(); + completer.complete('OK'); + expect(log).toEqual(['(', ')', '[', '(', ')', 'in(digest)', 'then(OK)', ']']); + }) + ); + + it('should allow microtasks in flush phase and process them immediatly', + async((Logger log, VmTurnZone zone, RootScope scope) { + scope.watch('g()', (_, __) {}); + scope.context['g'] = () { + log('!'); + return 0; + }; + + zone.run(() { + scope.domWrite(() { + log('domWriteA'); + return new Future.value(null).then((_) => scope.domWrite(() => log('domWriteB'))); + }); + }); + expect(log).toEqual( + ['[', '!', '!', 'domWriteA', '(', ')', 'domWriteB', /* assert */'!', ']']); + }) + ); + + it('should allow creation of Completers in flush phase', + async((Logger log, VmTurnZone zone, RootScope scope) { + Completer completer; + zone.run(() { + scope.domWrite(() { + log('new Completer'); + completer = new Completer(); + completer.future.then((value) { + log('then($value)'); + }); + }); + }); + log('='); + completer.complete('OK'); + log(';'); + expect(log).toEqual( + ['[', 'new Completer', ']', '=', '(', ')', '[', 'then(OK)', ']', ';']); + }) + ); + }); + + describe('domRead/domWrite', () { + beforeEachModule((Module module) { + module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler); + }); + + it('should run writes before reads', (RootScope rootScope, Logger logger, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e as LoggingExceptionHandler; + rootScope.domWrite(() { + logger('write1'); + rootScope.domWrite(() => logger('write2')); + throw 'write1'; + }); + rootScope.domRead(() { + logger('read1'); + rootScope.domRead(() => logger('read2')); + rootScope.domWrite(() => logger('write3')); + throw 'read1'; + }); + rootScope.watch('value', (_, __) => logger('observe'), + canChangeModel: false); + rootScope.flush(); + expect(logger).toEqual(['write1', 'write2', 'observe', 'read1', 'read2', 'write3']); + expect(exceptionHandler.errors.length).toEqual(2); + expect(exceptionHandler.errors[0].error).toEqual('write1'); + expect(exceptionHandler.errors[1].error).toEqual('read1'); + }); + + it("should run writes of child scopes first", (RootScope rootScope, Logger logger) { + final childScope = rootScope.createChild({}); + childScope.domWrite(() { + logger("child1"); + }); + rootScope.domWrite(() { + logger("root"); + }); + childScope.domWrite(() { + logger("child2"); + }); + + rootScope.flush(); + + expect(logger).toEqual(['child1', 'child2', 'root']); + }); + + it("should run reads of child scopes first", (RootScope rootScope, Logger logger) { + final childScope = rootScope.createChild({}); + childScope.domRead(() { + logger("child1"); + }); + rootScope.domRead(() { + logger("root"); + }); + childScope.domRead(() { + logger("child2"); + }); + + rootScope.flush(); + + expect(logger).toEqual(['child1', 'child2', 'root']); + }); + }); + + describe('exceptionHander', () { + beforeEachModule((Module module) { + module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler); + }); + + it('should call ExceptionHandler on zone errors', + async((RootScope rootScope, VmTurnZone zone, ExceptionHandler e) { + zone.run(() { + scheduleMicrotask(() => throw 'my error'); + }); + var errors = (e as LoggingExceptionHandler).errors; + expect(errors.length).toEqual(1); + expect(errors.first.error).toEqual('my error'); + })); + + it('should call ExceptionHandler on digest errors', + async((RootScope rootScope, VmTurnZone zone, ExceptionHandler e) { + rootScope.context['badOne'] = () => new Map(); + rootScope.watch('badOne()', (_, __) => null); + + try { + zone.run(() => null); + } catch(_) {} + + var errors = (e as LoggingExceptionHandler).errors; + expect(errors.length).toEqual(1); + expect(errors.first.error, startsWith('Model did not stabilize')); + })); + }); + + describe('logging', () { + it('should log a message on digest if reporting is enabled', (RootScope rootScope, + Injector injector) { + ScopeStatsConfig config = injector.get(ScopeStatsConfig); + config.emit = true; + rootScope.digest(); + expect((injector.get(ScopeStatsEmitter) as MockScopeStatsEmitter).invoked) + .toEqual(true); + }); + + it('should log a message on flush if reporting is enabled', (RootScope rootScope, + Injector injector) { + ScopeStatsConfig config = injector.get(ScopeStatsConfig); + config.emit = true; + rootScope.flush(); + expect((injector.get(ScopeStatsEmitter) as MockScopeStatsEmitter).invoked) + .toEqual(true); + }); + + it('should not log a message on digest if reporting is disabled', (RootScope rootScope, + Injector injector) { + rootScope.digest(); + expect((injector.get(ScopeStatsEmitter) as MockScopeStatsEmitter).invoked) + .toEqual(false); + }); + + it('should not log a message on flush if reporting is disabled', (RootScope rootScope, + Injector injector) { + rootScope.flush(); + expect((injector.get(ScopeStatsEmitter) as MockScopeStatsEmitter).invoked) + .toEqual(false); + }); + + it('can be turned on at runtime', (RootScope rootScope, Injector injector) { + rootScope.digest(); + expect((injector.get(ScopeStatsEmitter) as MockScopeStatsEmitter).invoked) + .toEqual(false); + ScopeStatsConfig config = injector.get(ScopeStatsConfig); + config.emit = true; + rootScope.digest(); + expect((injector.get(ScopeStatsEmitter) as MockScopeStatsEmitter).invoked) + .toEqual(true); + }); + }); + }); +} + +@Formatter(name: 'identity') +class _IdentityFormatter { + Logger logger; + _IdentityFormatter(this.logger); + call(v) { + logger('identity'); + return v; + } +} + +@Formatter(name: 'keys') +class _MapKeys { + Logger logger; + _MapKeys(this.logger); + call(Map m) { + logger('keys'); + return m.keys; + } +} + +@Formatter(name: 'multiply') +class _MultiplyFormatter { + call(a, b) => a * b; +} + +@Formatter(name: 'listHead') +class _ListHeadFormatter { + Logger logger; + _ListHeadFormatter(this.logger); + call(list, head) { + logger('listHead'); + return [head]..addAll(list); + } +} + +@Formatter(name: 'listTail') +class _ListTailFormatter { + Logger logger; + _ListTailFormatter(this.logger); + call(list, tail) { + logger('listTail'); + return new List.from(list)..add(tail); + } +} + +@Formatter(name: 'sort') +class _SortFormatter { + Logger logger; + _SortFormatter(this.logger); + call(list) { + logger('sort'); + return new List.from(list)..sort(); + } +} + +@Formatter(name:'newFormatter') +class FormatterOne { + call(String str) { + return '$str 1'; + } +} + +@Formatter(name:'newFormatter') +class FormatterTwo { + call(String str) { + return '$str 2'; + } +} + +class MockScopeStatsEmitter implements ScopeStatsEmitter { + bool invoked = false; + + void emitMessage(String message) {} + + void emitSummary(List digestTimes, int flushPhaseDuration, + int assertFlushPhaseDuration) {} + + void emit(String phaseOrLoopNo, AvgStopwatch fieldStopwatch, + AvgStopwatch evalStopwatch, AvgStopwatch processStopwatch) { + invoked = true; + } +} + +class UnstableList { + List get list => new List.generate(3, (i) => i); +} + +class Foo { + increment(x) => x+1; +} diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index 90c89440d..7cc9961a5 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -220,9 +220,10 @@ void main() { _.compile('
'); microLeap(); _.rootScope.apply(); + microLeap(); var component = _.rootScope.context['ioComponent']; - expect(component.scope.context['attr']).toEqual('A'); + expect(component.attr).toEqual('A'); })); it('should work with one-way bindings', async(() { @@ -231,9 +232,9 @@ void main() { microLeap(); _.rootScope.apply(); var component = _.rootScope.context['ioComponent']; - expect(component.scope.context['oneway']).toEqual('misko'); + expect(component.oneway).toEqual('misko'); - component.scope.context['oneway'] = 'angular'; + component.oneway = 'angular'; _.rootScope.apply(); // Not two-way, did not change. expect(_.rootScope.context['name']).toEqual('misko'); @@ -246,8 +247,9 @@ void main() { microLeap(); _.rootScope.apply(); var component = _.rootScope.context['ioComponent']; - expect(component.scope.context['expr']).toEqual('misko'); - component.scope.context['expr'] = 'angular'; + expect(component.attr).toBeNull(); + expect(component.expr).toEqual('misko'); + component.expr = 'angular'; _.rootScope.apply(); expect(_.rootScope.context['name']).toEqual('angular'); })); @@ -403,20 +405,20 @@ void main() { '' '
'); - final scope = _shadowScope(element.children[0]); + final component = ngProbe(element.children[0]).directive(ConditionalContentComponent); microLeap(); - scope.apply(); + _.rootScope.apply(); expect(element).toHaveText('(, ABC)'); - scope.context['showLeft'] = true; + component.showLeft = true; microLeap(); - scope.apply(); + _.rootScope.apply(); expect(element).toHaveText('(A, BC)'); - scope.context['showLeft'] = false; + component.showLeft = false; microLeap(); - scope.apply(); + _.rootScope.apply(); expect(element).toHaveText('(, ABC)'); })); @@ -508,7 +510,7 @@ void main() { expect(simpleElement).toHaveText('INNER(innerText)'); var simpleProbe = ngProbe(simpleElement); var simpleComponent = simpleProbe.injector.getByKey(new Key(SimpleComponent)); - expect(simpleComponent.scope.context['name']).toEqual('INNER'); + expect(simpleComponent.name).toEqual('INNER'); var shadowRoot = simpleElement.shadowRoot; // If there is no shadow root, skip this. @@ -604,14 +606,13 @@ void main() { _.rootScope.context['name'] = 'misko'; _.rootScope.apply(); var component = _.rootScope.context['ioComponent']; - expect(component.scope.context['name']).toEqual(null); - expect(component.scope.context['attr']).toEqual('A'); - expect(component.scope.context['expr']).toEqual('misko'); - component.scope.context['expr'] = 'angular'; + expect(component.attr).toEqual('A'); + expect(component.expr).toEqual('misko'); + component.expr = 'angular'; _.rootScope.apply(); expect(_.rootScope.context['name']).toEqual('angular'); expect(_.rootScope.context['done']).toEqual(null); - component.scope.context['ondone'](); + component.ondone(); expect(_.rootScope.context['done']).toEqual(true); })); @@ -635,8 +636,8 @@ void main() { var component = _.rootScope.context['ioComponent']; _.rootScope.apply(); - expect(component.scope.context['expr']).toEqual('misko'); - component.scope.context['expr'] = 'angular'; + expect(component.expr).toEqual('misko'); + component.expr = 'angular'; _.rootScope.apply(); expect(_.rootScope.context['name']).toEqual('angular'); })); @@ -739,8 +740,8 @@ void main() { _.compile(''); microLeap(); _.rootScope.apply(); - var componentScope = _.rootScope.context['camelCase']; - expect(componentScope.context['camelCase']).toEqual('G'); + var componentContext = _.rootScope.context['camelCase']; + expect(componentContext.camelCase).toEqual('G'); })); // TODO: This is a terrible test @@ -755,6 +756,13 @@ void main() { } })); + it('should publish component controller into the scope', async(() { + var element = _.compile(r'
'); + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('WORKED'); + })); + it('should "publish" controller to injector under provided module', () { _.compile(r'
'); expect(PublishModuleAttrDirective._injector.get(PublishModuleAttrDirective)). @@ -1012,7 +1020,6 @@ void main() { }); }); - describe('controller scoping', () { it('should make controllers available to sibling and child controllers', async((Logger log) { _.compile(''); @@ -1071,20 +1078,23 @@ void main() { _.rootScope.apply(); expect(log.result()).toEqual('Scope set'); - if (config.elementProbeEnabled) { - expect(ngInjector(element).get(ScopeAwareComponent). - scope.context['foo']).toEqual('bar'); + var controller = ngInjector(element).get(ScopeAwareComponent); + expect(controller.scope.context).toEqual(controller); } })); - it('should call scope setter on ScopeAware decorators', async((TestBed _, Logger log) { + it('should call scope setter on ScopeAware decorators', async((TestBed _, Logger log, CompilerConfig config) { var element = _.compile('
'); _.rootScope.apply(); expect(log.result()).toEqual('Scope set'); - expect(_.rootScope.context['foo']).toEqual('bar'); + + if (config.elementProbeEnabled) { + var scopePassedIn = ngInjector(element).get(ScopeAwareComponent).scope; + expect(scopePassedIn).toEqual(_.rootScope); + } })); }); @@ -1125,9 +1135,7 @@ void main() { visibility: Directive.DIRECT_CHILDREN_VISIBILITY) class TabComponent { int id = 0; - Logger log; - LocalAttrDirective local; - TabComponent(Logger this.log, LocalAttrDirective this.local, Scope scope) { + TabComponent(Logger log, LocalAttrDirective local) { log('TabComponent-${id++}'); local.ping(); } @@ -1139,7 +1147,7 @@ class TabComponent { ) class LazyPane { int id = 0; - LazyPane(Logger logger, LazyPaneHelper lph, Scope scope) { + LazyPane(Logger logger, LazyPaneHelper lph) { logger('LazyPane-${id++}'); } } @@ -1148,10 +1156,7 @@ class LazyPaneHelper {} @Component(selector: 'pane') class PaneComponent { - TabComponent tabComponent; - LocalAttrDirective localDirective; - Logger log; - PaneComponent(TabComponent this.tabComponent, LocalAttrDirective this.localDirective, Logger this.log, Scope scope) { + PaneComponent(TabComponent tabComponent, LocalAttrDirective localDirective, Logger log) { log('PaneComponent-${tabComponent.id++}'); localDirective.ping(); } @@ -1261,18 +1266,15 @@ class PublishModuleAttrDirective implements PublishModuleDirectiveSuperType { selector: 'simple', template: r'{{name}}()') class SimpleComponent { - Scope scope; - SimpleComponent(Scope this.scope) { - scope.context['name'] = 'INNER'; - } + var name = 'INNER'; } @Component( selector: 'multiple-content-tags', template: r'(, )') -class MultipleContentTagsComponent { - final Scope scope; - MultipleContentTagsComponent(this.scope); +class MultipleContentTagsComponent implements ScopeAware { + Scope scope; + MultipleContentTagsComponent(); } @Component( @@ -1298,35 +1300,46 @@ class TranscludingComponent { } @Component( - selector: 'conditional-content', - template: r'(
, )') -class ConditionalContentComponent { + selector: 'conditional-content', + template: r'(
, )') +class ConditionalContentComponent implements ScopeAware { Scope scope; - ConditionalContentComponent(this.scope); + var showLeft; + ConditionalContentComponent(); +} + +@Component( + selector: 'sometimes', + template: r'
') +class SometimesComponent { + @NgTwoWay('sometimes') + var sometimes; } @Component( selector: 'io', template: r'', map: const { - 'attr': '@scope.context.attr', - 'expr': '<=>scope.context.expr', - 'oneway': '=>scope.context.oneway', - 'ondone': '&scope.context.ondone', + 'attr': '@attr', + 'expr': '<=>expr', + 'oneway': '=>oneway', + 'ondone': '&ondone', }) -class IoComponent { - Scope scope; - IoComponent(Scope scope) { - this.scope = scope; +class IoComponent implements ScopeAware { + var attr; + var oneway; + var expr = 'initialExpr'; + Function ondone; + var done; + + void set scope(Scope scope) { scope.rootScope.context['ioComponent'] = this; - scope.context['expr'] = 'initialExpr'; } } @Component( selector: 'io-controller', template: r'', - publishAs: 'ctrl', map: const { 'attr': '@attr', 'expr': '<=>expr', @@ -1334,15 +1347,14 @@ class IoComponent { 'ondone': '&onDone', 'on-optional': '&onOptional' }) -class IoControllerComponent { - Scope scope; +class IoControllerComponent implements ScopeAware { var attr; var expr; var exprOnce; var onDone; var onOptional; - IoControllerComponent(Scope scope) { - this.scope = scope; + + void set scope(Scope scope) { scope.rootScope.context['ioComponent'] = this; } } @@ -1356,15 +1368,14 @@ class IoControllerComponent { 'ondone': '&onDone', 'onOptional': '&onOptional' }) -class UnpublishedIoControllerComponent { - Scope scope; +class UnpublishedIoControllerComponent implements ScopeAware { var attr; var expr; var exprOnce; var onDone; var onOptional; - UnpublishedIoControllerComponent(Scope scope) { - this.scope = scope; + + void set scope(Scope scope) { scope.rootScope.context['ioComponent'] = this; } } @@ -1384,12 +1395,13 @@ class NonAssignableMappingComponent { } @Component( selector: 'camel-case-map', map: const { - 'camel-case': '@scope.context.camelCase', + 'camel-case': '@camelCase', }) -class CamelCaseMapComponent { - Scope scope; - CamelCaseMapComponent(Scope this.scope) { - scope.rootScope.context['camelCase'] = scope; +class CamelCaseMapComponent implements ScopeAware { + var camelCase; + + void set scope(Scope scope) { + scope.rootScope.context['camelCase'] = this; } } @@ -1397,28 +1409,26 @@ class CamelCaseMapComponent { selector: 'parent-expression', template: '
inside {{fromParent()}}
', map: const { - 'from-parent': '&scope.context.fromParent', + 'from-parent': '&fromParent', }) class ParentExpressionComponent { Scope scope; - ParentExpressionComponent(Scope this.scope); + var fromParent; } @Component( selector: 'publish-me', - template: r'{{ctrlName.value}}', - publishAs: 'ctrlName') + template: r'{{value}}') class PublishMeComponent { String value = 'WORKED'; } @Component( selector: 'log', - template: r'', - publishAs: 'ctrlName') + template: r'') class LogComponent { - LogComponent(Scope scope, Logger logger) { - logger(scope); + LogComponent(Logger logger) { + logger("LogComponent"); } } @@ -1433,7 +1443,7 @@ class LogComponent { 'optional-two': '<=>optional', 'optional-once': '=>!optional', }) -class AttachDetachComponent implements AttachAware, DetachAware, ShadowRootAware { +class AttachDetachComponent implements AttachAware, DetachAware, ShadowRootAware, ScopeAware { Logger logger; Scope scope; String attrValue = 'too early'; @@ -1441,7 +1451,7 @@ class AttachDetachComponent implements AttachAware, DetachAware, ShadowRootAware String onceValue = 'too early'; String optional; - AttachDetachComponent(Logger this.logger, TemplateLoader templateLoader, Scope this.scope) { + AttachDetachComponent(this.logger, TemplateLoader templateLoader) { logger('new'); templateLoader.template.then((_) => logger('templateLoaded')); } @@ -1470,18 +1480,17 @@ class SayHelloFormatter { @Component( selector: 'expr-attr-component', template: r'', - publishAs: 'ctrl', map: const { 'expr': '<=>expr', 'one-way': '=>oneWay', 'once': '=>!exprOnce' }) -class ExprAttrComponent { +class ExprAttrComponent implements ScopeAware { var expr; var oneWay; var exprOnce; - ExprAttrComponent(Scope scope) { + void set scope(Scope scope) { scope.rootScope.context['exprAttrComponent'] = this; } } @@ -1491,11 +1500,18 @@ class ExprAttrComponent { templateUrl: 'foo.html') class SimpleAttachComponent implements AttachAware, ShadowRootAware { Logger logger; + SimpleAttachComponent(this.logger) { logger('SimpleAttachComponent'); } - attach() => logger('attach'); - onShadowRoot(_) => logger('onShadowRoot'); + + void attach() { + logger('attach'); + } + + void onShadowRoot(_) { + logger('onShadowRoot'); + } } @Decorator( @@ -1516,7 +1532,7 @@ class AttachWithAttr implements AttachAware { templateUrl: 'foo.html') class LogElementComponent{ LogElementComponent(Logger logger, Element element, Node node, - ShadowRoot shadowRoot) { + ShadowRoot shadowRoot) { logger(element); logger(node); logger(shadowRoot); @@ -1530,8 +1546,10 @@ class LogElementComponent{ }) class OneTimeDecorator { Logger log; + OneTimeDecorator(this.log); - set value(v) => log(v); + + void set value(v) => log(v); } @Decorator( @@ -1570,8 +1588,10 @@ class ScopeAwareComponent implements ScopeAware { ScopeAwareComponent(this.log) {} void set scope(Scope scope) { log('Scope set'); - scope.context['foo'] = 'bar'; _scope = scope; + // Despite a new scope is created with a ScopeAware component + // scope setter should not be called, in this case. + scope.createChild(this); } Scope get scope => _scope; } @@ -1589,8 +1609,7 @@ class InnerShadowy {} @Component( selector: 'once-inside', - template: '
', - publishAs: 'ctrl' + template: '
' ) class OnceInside { var ot; @@ -1618,27 +1637,27 @@ class InjectorDependentComponent { selector: 'outer-with-div', template: 'OUTER(
)' ) -class OuterWithDivComponent { - final Scope scope; - OuterWithDivComponent(this.scope); +class OuterWithDivComponent implements ScopeAware { + Scope scope; + OuterWithDivComponent(); } @Component( selector: 'outer', template: 'OUTER()' ) -class OuterComponent { - final Scope scope; - OuterComponent(this.scope); +class OuterComponent implements ScopeAware { + Scope scope; + OuterComponent(); } @Component( selector: 'inner', template: 'INNER()' ) -class InnerComponent { - final Scope scope; - InnerComponent(this.scope); +class InnerComponent implements ScopeAware { + Scope scope; + InnerComponent(); } @Component( @@ -1655,11 +1674,3 @@ class InnerInnerComponent { ) class TemplateUrlComponent { } - -_shadowScope(element){ - if (element.shadowRoot != null) { - return ngProbe(element.shadowRoot).scope; - } else { - return ngProbe(element).directives[0].scope; - } -} diff --git a/test/core_dom/event_handler_spec.dart b/test/core_dom/event_handler_spec.dart index 0c8e63b4e..3511527f6 100644 --- a/test/core_dom/event_handler_spec.dart +++ b/test/core_dom/event_handler_spec.dart @@ -5,11 +5,10 @@ import '../_specs.dart'; @Component(selector: 'bar', template: '''
- +
- ''', - publishAs: 'ctrl') + ''') class BarComponent { var invoked = false; BarComponent(RootScope scope) { @@ -77,7 +76,7 @@ main() { it('shoud handle event within content only once', async((TestBed _) { var e = compile(_, ''' -
+
'''); @@ -86,10 +85,9 @@ main() { document.querySelector('[on-abc]').dispatchEvent(new Event('abc')); var shadowRoot = document.querySelector('bar').shadowRoot; var shadowRootScope = _.getScope(shadowRoot); - BarComponent ctrl = shadowRootScope.context['ctrl']; - expect(ctrl.invoked).toEqual(false); + expect(shadowRootScope.context.invoked).toEqual(false); - expect(_.rootScope.context['ctrl']['invoked']).toEqual(true); + expect(_.rootScope.context['invoked']).toEqual(true); })); }); } diff --git a/test/core_dom/event_handler_spec.dart.orig b/test/core_dom/event_handler_spec.dart.orig new file mode 100644 index 000000000..3877e841a --- /dev/null +++ b/test/core_dom/event_handler_spec.dart.orig @@ -0,0 +1,105 @@ +library event_handler_spec; + +import '../_specs.dart'; + +@Component(selector: 'bar', + template: ''' +
+ + +
+ ''') +class BarComponent { + var invoked = false; + BarComponent(RootScope scope) { + scope.context['barComponent'] = this; + } +} + +main() { + describe('EventHandler', () { + Element ngAppElement; + beforeEachModule((Module module) { + ngAppElement = new DivElement()..attributes['ng-app'] = ''; + module..bind(BarComponent); + module..bind(Node, toValue: ngAppElement); + document.body.append(ngAppElement); + }); + + afterEach(() { + ngAppElement.remove(); + ngAppElement = null; + }); + + compile(_, html) { + ngAppElement.setInnerHtml(html, treeSanitizer: new NullTreeSanitizer()); + _.compile(ngAppElement); + return ngAppElement.firstChild; + } + + it('should register and handle event', (TestBed _) { + var e = compile(_, + '''
+
+
'''); + + _.triggerEvent(e.querySelector('[on-abc]'), 'abc'); + expect(_.rootScope.context['invoked']).toEqual(true); + }); + + it('shoud register and handle event with long name', (TestBed _) { + var e = compile(_, + '''
+
+
'''); + +<<<<<<< HEAD + _.triggerEvent(e, 'my-new-event'); +======= + _.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent'); +>>>>>>> feat(scope): component is the new context + expect(_.rootScope.context['invoked']).toEqual(true); + }); + + it('shoud have model updates applied correctly', (TestBed _) { + var e = compile(_, + '''
+
{{description}}
+
'''); + var el = document.querySelector('[on-abc]'); + el.dispatchEvent(new Event('abc')); + _.rootScope.apply(); + expect(e.text.trim()).toEqual("new description"); + }); + + it('shoud register event when shadow dom is used', async((TestBed _) { + var e = compile(_,''); + + microLeap(); + + var shadowRoot = e.shadowRoot; + var span = shadowRoot.querySelector('span'); + span.dispatchEvent(new CustomEvent('abc')); + BarComponent ctrl = _.rootScope.context['barComponent']; + expect(ctrl.invoked).toEqual(true); + })); + + it('shoud handle event within content only once', async((TestBed _) { + var e = compile(_, + '''
+ +
+
+ '''); + + microLeap(); + + document.querySelector('[on-abc]').dispatchEvent(new Event('abc')); + var shadowRoot = document.querySelector('bar').shadowRoot; + var shadowRootScope = _.getScope(shadowRoot); + expect(shadowRootScope.context.invoked).toEqual(false); + + expect(_.rootScope.context['invoked']).toEqual(true); + })); + }); +} diff --git a/test/core_dom/platform_js_based_shim_spec.dart b/test/core_dom/platform_js_based_shim_spec.dart index 192e4fb7b..5ab89f9d3 100644 --- a/test/core_dom/platform_js_based_shim_spec.dart +++ b/test/core_dom/platform_js_based_shim_spec.dart @@ -157,15 +157,20 @@ main() { @Component( selector: "test-wptc", - publishAs: "ctrl", templateUrl: "template.html", cssUrl: "style.css") class _WebPlatformTestComponent { } +@Component( + selector: "test-wptca[a]", + templateUrl: "template.html", + cssUrl: "style.css") +class _WebPlatformTestComponentWithAttribute { +} + @Component( selector: "my-inner", - publishAs: "ctrl", templateUrl: "inner-html.html", cssUrl: "inner-style.css") class _InnerComponent { @@ -173,7 +178,6 @@ class _InnerComponent { @Component( selector: "my-outer", - publishAs: "ctrl", templateUrl: "outer-html.html", cssUrl: "outer-style.css") class _OuterComponent { diff --git a/test/directive/ng_if_spec.dart b/test/directive/ng_if_spec.dart index 2ad47f030..b23f8f1d9 100644 --- a/test/directive/ng_if_spec.dart +++ b/test/directive/ng_if_spec.dart @@ -70,36 +70,57 @@ main() { } ); - they('should create a child scope', + they('should create and destroy a child scope', [ // ng-if '
' + '
'.trim() + - ' inside {{setBy}};'.trim() + + ' inside {{ctx}};'.trim() + '
'.trim() + - ' outside {{setBy}}'.trim() + + ' outside {{ctx}}'.trim() + '
', // ng-unless '
' + '
'.trim() + - ' inside {{setBy}};'.trim() + + ' inside {{ctx}};'.trim() + '
'.trim() + - ' outside {{setBy}}'.trim() + + ' outside {{ctx}}'.trim() + '
'], (html) { - rootScope.context['setBy'] = 'topLevel'; + rootScope.context['ctx'] = 'parent'; + + var getChildScope = () => rootScope.context['probe'] == null ? + null : rootScope.context['probe'].scope; + compile(html); - expect(element).toHaveText('outside topLevel'); + expect(element).toHaveText('outside parent'); + expect(getChildScope()).toBeNull(); + + rootScope.apply(() { + rootScope.context['isVisible'] = true; + }); + // The nested scope uses the parent context + expect(element).toHaveText('inside parent;outside parent'); + expect(element.querySelector('#outside')).toHaveHtml('outside parent'); + expect(element.querySelector('#inside')).toHaveHtml('inside parent;'); + + var childScope1 = getChildScope(); + expect(childScope1).toBeNotNull(); + var destroyListener = guinness.createSpy('destroy child scope'); + var watcher = childScope1.on(ScopeEvent.DESTROY).listen(destroyListener); + + rootScope.apply(() { + rootScope.context['isVisible'] = false; + }); + expect(getChildScope()).toBeNull(); + expect(destroyListener).toHaveBeenCalledOnce(); rootScope.apply(() { rootScope.context['isVisible'] = true; }); - expect(element).toHaveText('inside childController;outside topLevel'); - // The value on the parent scope.context['should'] be unchanged. - expect(rootScope.context['setBy']).toEqual('topLevel'); - expect(element.querySelector('#outside')).toHaveHtml('outside topLevel'); - // A child scope.context['must'] have been created and hold a different value. - expect(element.querySelector('#inside')).toHaveHtml('inside childController;'); + var childScope2 = getChildScope(); + expect(childScope2).toBeNotNull(); + expect(childScope2).not.toBe(childScope1); } ); diff --git a/test/directive/ng_include_spec.dart b/test/directive/ng_include_spec.dart index b1b793b70..7ff719f3c 100644 --- a/test/directive/ng_include_spec.dart +++ b/test/directive/ng_include_spec.dart @@ -13,12 +13,12 @@ main() { var element = _.compile('
'); - expect(element.innerHtml).toEqual(''); + expect(element).toHaveText(''); microLeap(); // load the template from cache. scope.context['name'] = 'Vojta'; scope.apply(); - expect(element.text).toEqual('my name is Vojta'); + expect(element).toHaveText('my name is Vojta'); })); it('should fetch template from url using interpolation', async((Scope scope, TemplateCache cache) { @@ -27,7 +27,7 @@ main() { var element = _.compile('
'); - expect(element.innerHtml).toEqual(''); + expect(element).toHaveText(''); scope.context['name'] = 'Vojta'; scope.context['template'] = 'tpl1.html'; @@ -35,15 +35,54 @@ main() { scope.apply(); microLeap(); scope.apply(); - expect(element.text).toEqual('My name is Vojta'); + expect(element).toHaveText('My name is Vojta'); scope.context['template'] = 'tpl2.html'; microLeap(); scope.apply(); microLeap(); scope.apply(); - expect(element.text).toEqual('I am Vojta'); + expect(element).toHaveText('I am Vojta'); })); + it('should create and destroy a child scope', async((Scope scope, TemplateCache cache) { + cache.put('tpl.html', new HttpResponse(200, '

include

')); + + var getChildScope = () => scope.context['probe'] == null ? + null : scope.context['probe'].scope; + + var element = _.compile('
'); + + expect(element).toHaveText(''); + expect(getChildScope()).toBeNull(); + + scope.context['template'] = 'tpl.html'; + microLeap(); + scope.apply(); + microLeap(); + scope.apply(); + expect(element).toHaveText('include'); + var childScope1 = getChildScope(); + expect(childScope1).toBeNotNull(); + var destroyListener = guinness.createSpy('destroy child scope'); + var watcher = childScope1.on(ScopeEvent.DESTROY).listen(destroyListener); + + scope.context['template'] = null; + microLeap(); + scope.apply(); + expect(element).toHaveText(''); + expect(getChildScope()).toBeNull(); + expect(destroyListener).toHaveBeenCalledOnce(); + + scope.context['template'] = 'tpl.html'; + microLeap(); + scope.apply(); + microLeap(); + scope.apply(); + expect(element).toHaveText('include'); + var childScope2 = getChildScope(); + expect(childScope2).toBeNotNull(); + expect(childScope2).not.toBe(childScope1); + })); }); } diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index c555df675..e5b032aaa 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -1596,8 +1596,7 @@ void main() { @Component( selector: 'no-love', - template: '', - publishAs: 'ctrl') + template: '') class ComponentWithNoLove { } diff --git a/test/directive/ng_repeat_spec.dart b/test/directive/ng_repeat_spec.dart index 1116e8c3e..5aa627e2e 100644 --- a/test/directive/ng_repeat_spec.dart +++ b/test/directive/ng_repeat_spec.dart @@ -404,9 +404,7 @@ main() { it('should not error when the first watched item is removed', () { element = compile( '
    ' - '
  • ' - r' ' - '
  • ' + '
  • {{ i }}
  • ' '
'); scope.context['items'] = ['misko', 'shyam', 'frodo']; scope.apply(); @@ -419,9 +417,7 @@ main() { it('should not error when the last watched item is removed', () { element = compile( '
    ' - '
  • ' - r' ' - '
  • ' + '
  • {{ i }}
  • ' '
'); scope.context['items'] = ['misko', 'shyam', 'frodo']; scope.apply(); @@ -434,15 +430,12 @@ main() { it('should not error when multiple watched items are removed at the same time', () { element = compile( '
    ' - '
  • ' - r' ' - '
  • ' + '
  • {{ i }}
  • ' '
'); scope.context['items'] = ['misko', 'shyam', 'frodo', 'igor']; scope.apply(); expect(element.children.length).toEqual(4); - scope.context['items'].remove('shyam'); - scope.context['items'].remove('frodo'); + scope.context['items']..remove('shyam')..remove('frodo'); scope.apply(); expect(element.children.length).toEqual(2); }); diff --git a/test/directive/ng_repeat_spec.dart.orig b/test/directive/ng_repeat_spec.dart.orig new file mode 100644 index 000000000..05104aaca --- /dev/null +++ b/test/directive/ng_repeat_spec.dart.orig @@ -0,0 +1,574 @@ +library ng_repeat_spec; + +import '../_specs.dart'; + +// Mock animate instance that throws on move +class MockAnimate extends Animate { + Animation move(Iterable nodes, Node parent, + {Node insertBefore}) { + throw "Move should not be called"; + } +} + +main() { + describe('NgRepeater', () { + Element element; + var compile, scope, exceptionHandler, directives; + + beforeEach((Injector injector, Scope rootScope, Compiler compiler, DirectiveMap _directives) { + exceptionHandler = injector.get(ExceptionHandler); + scope = rootScope; + compile = (html, [scope]) { + element = e(html); + ViewFactory viewFactory = compiler([element], _directives); + Injector blockInjector = injector; + if (scope != null) { + viewFactory.bind(null)(scope); + } else { + viewFactory(rootScope, null, [element]); + } + return element; + }; + directives = _directives; + }); + + it(r'should set create a list of items', (Scope scope, Compiler compiler, Injector injector) { + var element = es('
{{item}}
'); + ViewFactory viewFactory = compiler(element, directives); + View view = viewFactory(scope, null, element); + scope.context['items'] = ['a', 'b']; + scope.apply(); + expect(element).toHaveText('ab'); + }); + + + it(r'should set create a list of items', (Scope scope, Compiler compiler, Injector injector) { + scope.context['items'] = []; + scope.watch('1', (_, __) { + scope.context['items'].add('a'); + scope.context['items'].add('b'); + }); + var element = es('
{{item}}
'); + ViewFactory viewFactory = compiler(element, directives); + View view = viewFactory(scope, null, element); + scope.apply(); + expect(element).toHaveText('ab'); + }); + + + it(r'should set create a list of items from iterable', + (Scope scope, Compiler compiler, Injector injector) { + var element = es('
{{item}}
'); + ViewFactory viewFactory = compiler(element, directives); + View view = viewFactory(scope, null, element); + scope.context['items'] = ['a', 'b'].map((i) => i); // makes an iterable + scope.apply(); + expect(element).toHaveText('ab'); + }); + + + it(r'should iterate over an array of objects', () { + element = compile( + '
    ' + '
  • {{item.name}};
  • ' + '
'); + + // INIT + scope.context['items'] = [{"name": 'misko'}, {"name":'shyam'}]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(2); + expect(element.text).toEqual('misko;shyam;'); + + // GROW + scope.context['items'].add({"name": 'adam'}); + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('misko;shyam;adam;'); + + // SHRINK + scope.context['items'].removeLast(); + scope.context['items'].removeAt(0); + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(1); + expect(element.text).toEqual('shyam;'); + }); + + + it(r'should gracefully handle nulls', () { + element = compile( + '
' + '
    ' + '
  • {{item.name}};
  • ' + '
' + '
'); + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(0); + }); + + + it('should gracefully handle ref changing to null and back', () { + scope.context['items'] = ['odin', 'dva']; + element = compile( + '
' + '
    ' + '
  • {{item}};
  • ' + '
' + '
'); + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(2); + expect(element.text).toEqual('odin;dva;'); + + scope.context['items'] = null; + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(0); + expect(element.text).toEqual(''); + + scope.context['items'] = ['odin', 'dva', 'tri']; + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('odin;dva;tri;'); + }); + + it('should gracefully handle ref changing to non-list and back', () { + scope.context['items'] = ['odin', 'dva']; + element = compile( + '
' + '
    ' + '
  • {{item}};
  • ' + '
' + '
'); + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(2); + expect(element.text).toEqual('odin;dva;'); + + scope.context['items'] = 'string'; + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(0); + expect(element.text).toEqual(''); + + scope.context['items'] = ['odin', 'dva', 'tri']; + scope.apply(); + expect(element.querySelectorAll('ul').length).toEqual(1); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('odin;dva;tri;'); + }); + + + it('should support formatters', () { + element = compile( + '
{{item}}
'); + scope.context['items'] = ['foo', 'bar', 'baz']; + scope.context['myFilter'] = (String item) => item.startsWith('b'); + scope.apply(); + expect(element.querySelectorAll('span').length).toEqual(2); + }); + + it('should support function as a formatter', () { + scope.context['isEven'] = (num) => num % 2 == 0; + var element = compile( + '
' + '{{r}}' + '
'); + scope.apply(); + expect(element.text).toEqual('2'); + }); + + + describe('track by', () { + it(r'should track using expression function', () { + element = compile( + '
    ' + '
  • {{item.name}};
  • ' + '
'); + scope.context['items'] = [{"id": 'misko'}, {"id": 'igor'}]; + scope.apply(); + var li0 = element.querySelectorAll('li')[0]; + var li1 = element.querySelectorAll('li')[1]; + + scope.context['items'].add(scope.context['items'].removeAt(0)); + scope.apply(); + expect(element.querySelectorAll('li')[0]).toBe(li1); + expect(element.querySelectorAll('li')[1]).toBe(li0); + }); + + + it(r'should track using build in $id function', () { + element = compile( + '
    ' + r'
  • {{item.name}};
  • ' + '
'); + scope.context['items'] = [{"name": 'misko'}, {"name": 'igor'}]; + scope.apply(); + var li0 = element.querySelectorAll('li')[0]; + var li1 = element.querySelectorAll('li')[1]; + + scope.context['items'].add(scope.context['items'].removeAt(0)); + scope.apply(); + expect(element.querySelectorAll('li')[0]).toBe(li1); + expect(element.querySelectorAll('li')[1]).toBe(li0); + }); + + + it(r'should iterate over an array of primitives', () { + element = compile( + r'
    ' + r'
  • {{item}};
  • ' + r'
'); + + // INIT + scope.context['items'] = [true, true, true]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('true;true;true;'); + + scope.context['items'] = [false, true, true]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('false;true;true;'); + + scope.context['items'] = [false, true, false]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('false;true;false;'); + + scope.context['items'] = [true]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(1); + expect(element.text).toEqual('true;'); + + scope.context['items'] = [true, true, false]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('true;true;false;'); + + scope.context['items'] = [true, false, false]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('true;false;false;'); + + // string + scope.context['items'] = ['a', 'a', 'a']; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('a;a;a;'); + + scope.context['items'] = ['ab', 'a', 'a']; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('ab;a;a;'); + + scope.context['items'] = ['test']; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(1); + expect(element.text).toEqual('test;'); + + scope.context['items'] = ['same', 'value']; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(2); + expect(element.text).toEqual('same;value;'); + + // number + scope.context['items'] = [12, 12, 12]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('12;12;12;'); + + scope.context['items'] = [53, 12, 27]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(3); + expect(element.text).toEqual('53;12;27;'); + + scope.context['items'] = [89]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(1); + expect(element.text).toEqual('89;'); + + scope.context['items'] = [89, 23]; + scope.apply(); + expect(element.querySelectorAll('li').length).toEqual(2); + expect(element.text).toEqual('89;23;'); + }); + + }); + + + it(r'should error on wrong parsing of ngRepeat', () { + expect(() { + compile('
')(); + }).toThrowWith(message: "[NgErr7] ngRepeat error! Expected expression in form of " + "'_item_ in _collection_[ track by _id_]' but got " + "'i dont parse'."); + }); + + + it("should throw error when left-hand-side of ngRepeat can't be parsed", () { + expect(() { + compile('
')(); + }).toThrowWith(message: "[NgErr8] ngRepeat error! '_item_' in '_item_ in " + "_collection_' should be an identifier or '(_key_, _value_)' " + "expression, but got 'i dont parse'."); + }); + + + it(r'should expose iterator offset as $index when iterating over arrays', + () { + element = compile( + '
    ' + + '
  • {{item}}:{{\$index}}|
  • ' + + '
'); + scope.context['items'] = ['misko', 'shyam', 'frodo']; + scope.apply(); + expect(element.text).toEqual('misko:0|shyam:1|frodo:2|'); + }); + + it(r'should expose iterator position as $first, $middle and $last when iterating over arrays', + () { + element = compile( + '
    ' + '
  • {{item}}:{{\$first}}-{{\$middle}}-{{\$last}}|
  • ' + '
'); + scope.context['items'] = ['misko', 'shyam', 'doug']; + scope.apply(); + expect(element.text) + .toEqual('misko:true-false-false|' + 'shyam:false-true-false|' + 'doug:false-false-true|'); + + scope.context['items'].add('frodo'); + scope.apply(); + expect(element.text) + .toEqual('misko:true-false-false|' + 'shyam:false-true-false|' + 'doug:false-true-false|' + 'frodo:false-false-true|'); + + scope.context['items'].removeLast(); + scope.context['items'].removeLast(); + scope.apply(); + + expect(element.text).toEqual('misko:true-false-false|' + 'shyam:false-false-true|'); + scope.context['items'].removeLast(); + scope.apply(); + expect(element.text).toEqual('misko:true-false-true|'); + }); + + it(r'should report odd', () { + element = compile( + '
    ' + '
  • {{item}}:{{\$odd}}-{{\$even}}|
  • ' + '
'); + scope.context['items'] = ['misko', 'shyam', 'doug']; + scope.apply(); + expect(element.text).toEqual('misko:false-true|' + 'shyam:true-false|' + 'doug:false-true|'); + + scope.context['items'].add('frodo'); + scope.apply(); + expect(element.text).toEqual('misko:false-true|' + 'shyam:true-false|' + 'doug:false-true|' + 'frodo:true-false|'); + + scope.context['items'].removeLast(); + scope.context['items'].removeLast(); + scope.apply(); + expect(element.text).toEqual('misko:false-true|shyam:true-false|'); + + scope.context['items'].removeLast(); + scope.apply(); + expect(element.text).toEqual('misko:false-true|'); + }); + + it(r'should repeat over nested arrays', () { + element = compile( + '
    ' + + '
  • ' + + '
    {{group}}|
    X' + + '
  • ' + + '
'); + scope.context['groups'] = [['a', 'b'], ['c','d']]; + scope.apply(); + + expect(element.text).toEqual('a|b|Xc|d|X'); + }); + + describe('nested watching', () { + it('should not error when the first watched item is removed', () { + element = compile( + '
    ' + '
  • {{ i }}
  • ' + '
'); + scope.context['items'] = ['misko', 'shyam', 'frodo']; + scope.apply(); + expect(element.children.length).toEqual(3); + scope.context['items'].remove('misko'); + scope.apply(); + expect(element.children.length).toEqual(2); + }); + + it('should not error when the last watched item is removed', () { + element = compile( + '
    ' + '
  • {{ i }}
  • ' + '
'); + scope.context['items'] = ['misko', 'shyam', 'frodo']; + scope.apply(); + expect(element.children.length).toEqual(3); + scope.context['items'].remove('frodo'); + scope.apply(); + expect(element.children.length).toEqual(2); + }); + + it('should not error when multiple watched items are removed at the same time', () { + element = compile( + '
    ' + '
  • {{ i }}
  • ' + '
'); + scope.context['items'] = ['misko', 'shyam', 'frodo', 'igor']; + scope.apply(); + expect(element.children.length).toEqual(4); + scope.context['items']..remove('shyam')..remove('frodo'); + scope.apply(); + expect(element.children.length).toEqual(2); + }); + }); + + describe('stability', () { + var a, b, c, d, lis; + + beforeEach(() { + element = compile( + '
    ' + r'
  • {{ $index }}
  • ' + '
'); + a = {}; + b = {}; + c = {}; + d = {}; + + scope.context['items'] = [a, b, c]; + scope.apply(); + lis = element.querySelectorAll('li'); + }); + + it(r'should correctly update rows orders - gh1154', () { + scope.context['items'] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + scope.apply(); + expect(element).toHaveText('0123456789'); + scope.context['items'] = [1, 2, 6, 7, 4, 3, 5, 8, 9, 0]; + scope.apply(); + expect(element).toHaveText('0123456789'); + }); + + it(r'should preserve the order of elements', () { + scope.context['items'] = [a, c, d]; + scope.apply(); + var newElements = element.querySelectorAll('li'); + expect(newElements[0]).toEqual(lis[0]); + expect(newElements[1]).toEqual(lis[2]); + expect(newElements[2]).not.toEqual(lis[1]); + }); + + it(r'should not throw an error on duplicates', () { + scope.context['items'] = [a, a, a]; + expect(() => scope.apply()).not.toThrow(); + scope.context['items'].add(a); + expect(() => scope.apply()).not.toThrow(); + }); + + it(r'should reverse items when the collection is reversed', () { + scope.context['items'] = [a, b, c]; + scope.apply(); + lis = element.querySelectorAll('li'); + + scope.context['items'] = [c, b, a]; + scope.apply(); + var newElements = element.querySelectorAll('li'); + expect(newElements.length).toEqual(3); + expect(newElements[0]).toEqual(lis[2]); + expect(newElements[1]).toEqual(lis[1]); + expect(newElements[2]).toEqual(lis[0]); + }); + + + it(r'should reuse elements even when model is composed of primitives', () { + // rebuilding repeater from scratch can be expensive, we should try to + // avoid it even for model that is composed of primitives. + + scope.context['items'] = ['hello', 'cau', 'ahoj']; + scope.apply(); + lis = element.querySelectorAll('li'); + lis[2].id = 'yes'; + + scope.context['items'] = ['ahoj', 'hello', 'cau']; + scope.apply(); + var newLis = element.querySelectorAll('li'); + expect(newLis.length).toEqual(3); + expect(newLis[0]).toEqual(lis[2]); + expect(newLis[1]).toEqual(lis[0]); + expect(newLis[2]).toEqual(lis[1]); + }); + }); + + + it('should correctly handle detached state', () { + scope.context['items'] = [1]; + +<<<<<<< HEAD + var parentScope = scope.createProtoChild(); +======= + var childScope = scope.createChild(scope.context); +>>>>>>> feat(scope): component is the new context + element = compile( + '
    ' + '
  • {{item}}
  • ' + '
', childScope); + + childScope.destroy(); + expect(scope.apply).not.toThrow(); + }); + + it(r'should not move blocks when elements only added or removed', + (Injector injector, Scope rootScope, Compiler compiler, + DirectiveMap _directives, ExceptionHandler exceptionHandler) { + var throwOnMove = new MockAnimate(); + var child = injector.createChild( + [new Module()..bind(Animate, toValue: throwOnMove)]); + + scope = rootScope; + compile = (html) { + element = e(html); + var viewFactory = compiler([element], _directives); + viewFactory(scope, null, [element]); + return element; + }; + directives = _directives; + + element = compile( + '
    ' + '
  • {{item}}
  • ' + '
'); + + scope..context['items'] = ['a', 'b', 'c'] + ..apply() + // grow + ..context['items'].add('d') + ..apply() + // shrink + ..context['items'].removeLast() + ..apply() + ..context['items'].removeAt(0) + ..apply(); + + expect(element).toHaveText('bc'); + }); + }); +} diff --git a/test/directive/ng_switch_spec.dart b/test/directive/ng_switch_spec.dart index 2c26666ca..2ab47c728 100644 --- a/test/directive/ng_switch_spec.dart +++ b/test/directive/ng_switch_spec.dart @@ -174,7 +174,7 @@ void main() { _.rootScope.apply(); var getChildScope = () => _.rootScope.context['probe'] == null ? - null : _.rootScope.context['probe'].scope; + null : _.rootScope.context['probe'].scope; expect(getChildScope()).toBeNull(); diff --git a/test/io/expression_extractor_spec.dart b/test/io/expression_extractor_spec.dart index 6a2711270..9b1e12cc2 100644 --- a/test/io/expression_extractor_spec.dart +++ b/test/io/expression_extractor_spec.dart @@ -33,19 +33,15 @@ void main() { var expressions = _extractExpressions('test/io/test_files/main.dart'); expect(expressions, unorderedEquals([ - 'ctrl.expr', - 'ctrl.anotherExpression', - 'ctrl.callback', - 'ctrl.twoWayStuff', 'attr', 'expr', 'anotherExpression', 'callback', 'twoWayStuff', 'exported + expression', - 'ctrl.inline.template.expression', + 'inline.template.expression', 'ngIfCondition', - 'ctrl.if' + 'if' ])); }); diff --git a/test/io/test_files/main.dart b/test/io/test_files/main.dart index cd263cdfd..094684265 100644 --- a/test/io/test_files/main.dart +++ b/test/io/test_files/main.dart @@ -16,7 +16,7 @@ class NgIfDirective { 'attr': '@attr', 'expr': '=>expr' }, - template: '
{{ctrl.inline.template.expression}}
', + template: '
{{inline.template.expression}}
', exportExpressionAttrs: const ['exported-attr'], exportExpressions: const ['exported + expression']) class MyComponent { diff --git a/test/io/test_files/main.html b/test/io/test_files/main.html index 95ecc8ad0..41b1ff188 100644 --- a/test/io/test_files/main.html +++ b/test/io/test_files/main.html @@ -1,16 +1,15 @@
- + + attr="attr2" expr="expr2" + another-expression="anotherExpression2" + callback="callback2" + two-way-stuff="twoWayStuff2"> -
-
+
\ No newline at end of file diff --git a/test/routing/ng_view_spec.dart b/test/routing/ng_view_spec.dart index 34bffa8f8..c0fd88623 100644 --- a/test/routing/ng_view_spec.dart +++ b/test/routing/ng_view_spec.dart @@ -20,28 +20,26 @@ main() => describe('ngView', () { _ = tb; router = _router; - templates.put('foo.html', new HttpResponse(200, - '

Foo

')); - templates.put('bar.html', new HttpResponse(200, - '

Bar

')); + templates.put('foo.html', new HttpResponse(200, '

Foo

')); + templates.put('bar.html', new HttpResponse(200, '

Bar

')); }); it('should switch template', async(() { Element root = _.compile(''); - expect(root.text).toEqual(''); + expect(root).toHaveText(''); router.route('/foo'); microLeap(); - expect(root.text).toEqual('Foo'); + expect(root).toHaveText('Foo'); router.route('/bar'); microLeap(); - expect(root.text).toEqual('Bar'); + expect(root).toHaveText('Bar'); router.route('/foo'); microLeap(); - expect(root.text).toEqual('Foo'); + expect(root).toHaveText('Foo'); })); it('should expose NgView as RouteProvider', async(() { @@ -62,25 +60,50 @@ main() => describe('ngView', () { router.route('/foo'); microLeap(); Element root = _.compile(''); - expect(root.text).toEqual(''); + expect(root).toHaveText(''); _.rootScope.apply(); microLeap(); - expect(root.text).toEqual('Foo'); + expect(root).toHaveText('Foo'); })); it('should clear template when route is deactivated', async(() { Element root = _.compile(''); - expect(root.text).toEqual(''); + expect(root).toHaveText(''); router.route('/foo'); microLeap(); - expect(root.text).toEqual('Foo'); + expect(root).toHaveText('Foo'); router.route('/baz'); // route without a template microLeap(); - expect(root.text).toEqual(''); + expect(root).toHaveText(''); + })); + + it('should create and destroy a child scope', async((RootScope scope) { + Element root = _.compile(''); + + var getChildScope = () => scope.context['p'] == null ? + null : scope.context['p'].scope; + + expect(root).toHaveText(''); + expect(getChildScope()).toBeNull(); + + router.route('/foo'); + microLeap(); + expect(root).toHaveText('Foo'); + var childScope1 = getChildScope(); + expect(childScope1).toBeNotNull(); + var destroyListener = guinness.createSpy('destroy child scope'); + var watcher = childScope1.on(ScopeEvent.DESTROY).listen(destroyListener); + + router.route('/baz'); + microLeap(); + expect(root).toHaveText(''); + expect(destroyListener).toHaveBeenCalledOnce(); + var childScope2 = getChildScope(); + expect(childScope2).toBeNull(); })); }); @@ -116,25 +139,25 @@ main() => describe('ngView', () { it('should switch nested templates', async(() { Element root = _.compile(''); microLeap(); _.rootScope.apply(); microLeap(); - expect(root.text).toEqual(''); + expect(root).toHaveText(''); router.route('/library/all'); microLeap(); _.rootScope.apply(); microLeap(); - expect(root.text).toEqual('LibraryBooks'); + expect(root).toHaveText('LibraryBooks'); router.route('/library/1234'); microLeap(); _.rootScope.apply(); microLeap(); - expect(root.text).toEqual('LibraryBook 1234'); + expect(root).toHaveText('LibraryBook 1234'); // nothing should change here router.route('/library/1234/overview'); microLeap(); _.rootScope.apply(); microLeap(); - expect(root.text).toEqual('LibraryBook 1234'); + expect(root).toHaveText('LibraryBook 1234'); // nothing should change here router.route('/library/1234/read'); microLeap(); _.rootScope.apply(); microLeap(); - expect(root.text).toEqual('LibraryRead Book 1234'); + expect(root).toHaveText('LibraryRead Book 1234'); })); it('should not attempt to destroy and already destroyed childscope', async(() { @@ -182,11 +205,11 @@ main() => describe('ngView', () { it('should switch inline templates', async(() { Element root = _.compile(''); - expect(root.text).toEqual(''); + expect(root).toHaveText(''); router.route('/foo'); microLeap(); - expect(root.text).toEqual('Hello'); + expect(root).toHaveText('Hello'); })); }); }); diff --git a/test/tools/html_extractor_spec.dart b/test/tools/html_extractor_spec.dart index 253059d11..012eb1529 100644 --- a/test/tools/html_extractor_spec.dart +++ b/test/tools/html_extractor_spec.dart @@ -13,40 +13,40 @@ void main() { it('should extract text mustache expressions', () { var ioService = new MockIoService({ 'foo.html': r''' -
foo {{ctrl.bar}} baz {{aux}}
+
foo {{bar}} baz {{aux}}
''' }); var extractor = new HtmlExpressionExtractor([]); extractor.crawl('/', ioService); expect(extractor.expressions.toList()..sort(), - equals(['aux', 'ctrl.bar'])); + equals(['aux', 'bar'])); }); it('should extract attribute mustache expressions', () { var ioService = new MockIoService({ 'foo.html': r''' -
+
''' }); var extractor = new HtmlExpressionExtractor([]); extractor.crawl('/', ioService); expect(extractor.expressions.toList()..sort(), - equals(['aux', 'ctrl.bar'])); + equals(['aux', 'bar'])); }); it('should extract ng-repeat expressions', () { var ioService = new MockIoService({ 'foo.html': r''' -
+
''' }); var extractor = new HtmlExpressionExtractor([]); extractor.crawl('/', ioService); expect(extractor.expressions.toList()..sort(), - equals(['ctrl.bar'])); + equals(['bar'])); }); it('should extract expressions provided in the directive info', () { @@ -61,30 +61,30 @@ void main() { }); it('should extract expressions from expression attributes', () { - var ioService = new MockIoService({'foo.html': r''}); + var ioService = new MockIoService({ + 'foo.html': r'' + }); var extractor = new HtmlExpressionExtractor([ new DirectiveInfo('foo', ['bar']) ]); extractor.crawl('/', ioService); - expect(extractor.expressions.toList()).toEqual(['ctrl.baz']); + expect(extractor.expressions.toList()).toEqual(['baz']); }); it('should extract expressions from expression attributes for camelCased attributes', () { - var ioService = new MockIoService({'foo.html': r''}); + var ioService = new MockIoService({'foo.html': r''}); var extractor = new HtmlExpressionExtractor([ new DirectiveInfo('foo', ['fooBar']) ]); extractor.crawl('/', ioService); - expect(extractor.expressions.toList()).toEqual(['ctrl.baz']); + expect(extractor.expressions.toList()).toEqual(['baz']); }); it('should ignore ng-repeat while extracting attribute expressions', () { var ioService = new MockIoService({ - 'foo.html': r''' -
- ''' + 'foo.html': r'
' }); var extractor = new HtmlExpressionExtractor([ @@ -93,7 +93,7 @@ void main() { extractor.crawl('/', ioService); // Basically we don't want to extract "foo in ctrl.bar". expect(extractor.expressions.toList()..sort(), - equals(['ctrl.bar'])); + equals(['bar'])); }); }); } diff --git a/test_transformers/web/relative_uris/foo2/relative_foo.dart b/test_transformers/web/relative_uris/foo2/relative_foo.dart index d87e32b93..3833582b4 100644 --- a/test_transformers/web/relative_uris/foo2/relative_foo.dart +++ b/test_transformers/web/relative_uris/foo2/relative_foo.dart @@ -5,8 +5,6 @@ import 'package:angular/angular.dart'; @Component( selector: 'relative-foo', useShadowDom: false, - templateUrl: 'relative_foo.html', - publishAs: 'ctrl', - applyAuthorStyles: true) + templateUrl: 'relative_foo.html') class RelativeFooComponent { }