diff --git a/lib/angular.dart b/lib/angular.dart index 3529cf471..9806e4d97 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -171,7 +171,8 @@ bootstrapAngular(modules, [rootElementSelector = '[ng-app]']) { Injector injector = new Injector(modules); injector.invoke((Compiler $compile, Scope $rootScope) { - $compile(topElt)(injector, topElt); - $rootScope.$digest(); + $rootScope.$apply(() { + $compile(topElt)(injector, topElt); + }); }); } diff --git a/lib/block.dart b/lib/block.dart index 5c336321b..17eca3db2 100644 --- a/lib/block.dart +++ b/lib/block.dart @@ -304,7 +304,6 @@ class ComponentFactory { attachBlockToShadowDom(BlockType blockType) { var block = blockType(shadowInjector); shadowDom.nodes.addAll(block.elements); - shadowInjector.get(Scope).$digest(); return shadowDom; } diff --git a/lib/directives/ng_include.dart b/lib/directives/ng_include.dart index 6c87e3e3d..734a65a07 100644 --- a/lib/directives/ng_include.dart +++ b/lib/directives/ng_include.dart @@ -47,9 +47,6 @@ class NgIncludeAttrDirective { // an url template blockCache.fromUrl(value).then((createBlock) { updateContent(createBlock); - - // Http should take care of this - scope.$digest(); }); } }); diff --git a/lib/scope.dart b/lib/scope.dart index dbc1c7ac7..c5dc55891 100644 --- a/lib/scope.dart +++ b/lib/scope.dart @@ -173,6 +173,8 @@ class Scope implements Map { } + + $digest() { var value, last, asyncQueue = _asyncQueue, @@ -283,20 +285,53 @@ class Scope implements Map { $apply([expr]) { - try { - _beginPhase('\$apply'); - return $eval(expr); - } catch (e, s) { - _exceptionHandler(e, s); - } finally { - _clearPhase(); + var toThrow; + var returnValue; + bool executingAsyncCallback = false; + async.runZonedExperimental(() { try { - $root.$digest(); + _beginPhase('\$apply'); + returnValue = $eval(expr); } catch (e, s) { _exceptionHandler(e, s); - throw e; + } finally { + _clearPhase(); + try { + $root.$digest(); + } catch (e, s) { + _exceptionHandler(e, s); + throw e; + } + } + }, onRunAsync: (fn) { + async.runAsync(() { + // exceptions from fn() can not be caught here, but + // are handled by onError. + executingAsyncCallback = true; + fn(); + executingAsyncCallback = false; + try { + $root.$digest(); + } catch (e, s) { + _exceptionHandler(e, s); + } + }); + }, + onError: (e) { + if (executingAsyncCallback) { + // NOTE(deboer): Thrown strings don't have an attached stack trace + // http://dartbug.com/12000 + _exceptionHandler(e, async.getAttachedStackTrace(e)); } + if (toThrow == null) + toThrow = e; + }); + // This will only throw exceptions from the runZoned body. + // The async code will be executed after this check. + if (toThrow != null) { + throw toThrow; } + return returnValue; } diff --git a/test/compiler_spec.dart b/test/compiler_spec.dart index 1973738f4..b6eb2d860 100644 --- a/test/compiler_spec.dart +++ b/test/compiler_spec.dart @@ -412,8 +412,9 @@ main() { $rootScope.sep = '-'; var element = $(r'
{{name}}{{sep}}{{$id}}:{{name}}{{sep}}{{$id}}
'); BlockType blockType = $compile(element); - Block block = blockType(injector, element); - $rootScope.$digest(); + $rootScope.$apply(() { + Block block = blockType(injector, element); + }); nextTurn(); expect(element.textWithShadow()).toEqual('OUTTER-_1:INNER_2(OUTTER-_1)'); @@ -424,7 +425,9 @@ main() { $rootScope.val = "poof"; var element = $(''); - $compile(element)(injector, element); + $rootScope.$apply(() { + $compile(element)(injector, element); + }); nextTurn(); expect(renderedText(element)).toEqual('inside poof'); @@ -432,7 +435,9 @@ main() { it('should behave nicely if a mapped attribute is missing', async(inject(() { var element = $(''); - $compile(element)(injector, element); + $rootScope.$apply(() { + $compile(element)(injector, element); + }); nextTurn(); expect(renderedText(element)).toEqual('inside '); @@ -441,7 +446,9 @@ main() { it('should behave nicely if a mapped attribute evals to null', async(inject(() { $rootScope.val = null; var element = $(''); - $compile(element)(injector, element); + $rootScope.$apply(() { + $compile(element)(injector, element); + }); nextTurn(); expect(renderedText(element)).toEqual('inside '); @@ -491,10 +498,11 @@ main() { }, throwsA(contains('No provider found for LocalAttrDirective! (resolving LocalAttrDirective)'))); })); - it('should publish component controller into the scope', async(inject(() { + it('should publish component controller into the scope', async(inject((Scope $rootScope) { var element = $(r'
'); - $compile(element)(injector, element); - $rootScope.$apply(); + $rootScope.$apply(() { + $compile(element)(injector, element); + }); nextTurn(); expect(element.textWithShadow()).toEqual('WORKED'); diff --git a/test/scope_spec.dart b/test/scope_spec.dart index 3c57cb01c..a14944a61 100644 --- a/test/scope_spec.dart +++ b/test/scope_spec.dart @@ -1,4 +1,5 @@ import "_specs.dart"; +import "dart:async"; main() { describe(r'Scope', () { @@ -482,6 +483,71 @@ main() { }); }); + describe(r'async', () { + beforeEach(module((AngularModule module) { + return module.type(ExceptionHandler, LogExceptionHandler); + })); + + it(r'should run watch cycle on future resolution', async(inject((Scope scope) { + var log = ''; + scope['r'] = 0; + scope['qq'] = 0; + scope.$watch('q', (_) { + log += 'q'; + scope['qq'] = 1; + new Future.value('b').then((v) { + log += 'b'; + scope['r'] = 3; + }); + }); + + scope.$watch('r', (rVal) { + log += 'r'; + scope['s'] = rVal * 2; + }); + scope.$apply("q=2"); + expect(scope['qq']).toEqual(1); + expect(scope['q']).toEqual(2); + expect(scope['r']).toEqual(0); + expect(log).toEqual('qr'); + nextTurn(true); + + expect(scope['r']).toEqual(3); + expect(scope['s']).toEqual(6); + expect(log).toEqual('qrbr'); + }))); + + + it(r'should catch exceptions from futures', async(inject((Scope scope, ExceptionHandler $exceptionHandler) { + scope.$watch('q', (_) { + scope['qq'] = 1; + new Future.value('b').then((v) { + throw "sadface"; + }); + }); + scope.$apply("q=2"); + nextTurn(true); + expect($exceptionHandler.errors.length).toEqual(1); + expect($exceptionHandler.errors[0].error).toEqual("sadface"); + }))); + + it(r'should catch exceptions from digests trigger by futures', async(inject((Scope scope, ExceptionHandler $exceptionHandler) { + scope.$watch('q', (_) { + scope['qq'] = 1; + new Future.value('b').then((v) { + scope['x'] = 2; + }); + }); + scope.$watch('x', (xVal) { + if (xVal == 2) throw "x was 2"; + }); + scope.$apply("q=2"); + nextTurn(true); + expect($exceptionHandler.errors.length).toEqual(1); + expect($exceptionHandler.errors[0].error).toEqual("x was 2"); + }))); + }); + describe(r'exceptions', () { var log;