Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

feat(scope): Automatically $digest futures inside of $apply. #55

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/angular.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}
1 change: 0 additions & 1 deletion lib/block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ class ComponentFactory {
attachBlockToShadowDom(BlockType blockType) {
var block = blockType(shadowInjector);
shadowDom.nodes.addAll(block.elements);
shadowInjector.get(Scope).$digest();
return shadowDom;
}

Expand Down
3 changes: 0 additions & 3 deletions lib/directives/ng_include.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ class NgIncludeAttrDirective {
// an url template
blockCache.fromUrl(value).then((createBlock) {
updateContent(createBlock);

// Http should take care of this
scope.$digest();
});
}
});
Expand Down
53 changes: 44 additions & 9 deletions lib/scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ class Scope implements Map {
}




$digest() {
var value, last,
asyncQueue = _asyncQueue,
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it you throw toThrow, then you are starting a new stack trace. What you want to do is to re-throw, which is just throw; with no arg.

but if toThrow is null then you eat exception? Can you explain this logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toThrow is passing the exception from the onError handler out to the $apply function.

If we rethrow the exception in the onError handler, the runZoned runner picks it up and we never see it again.

The "toThrow == null" is there so we throw the first exception, since we can only throw one exception in the $apply function. [ I should write a test for this behaviour... ]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, apparently throw; is deprecated. Gotta use "rethrow".

}
return returnValue;
}


Expand Down
24 changes: 16 additions & 8 deletions test/compiler_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,9 @@ main() {
$rootScope.sep = '-';
var element = $(r'<div>{{name}}{{sep}}{{$id}}:<simple>{{name}}{{sep}}{{$id}}</simple></div>');
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)');
Expand All @@ -424,15 +425,19 @@ main() {
$rootScope.val = "poof";
var element = $('<parent-expression from-parent=val></parent-expression>');

$compile(element)(injector, element);
$rootScope.$apply(() {
$compile(element)(injector, element);
});

nextTurn();
expect(renderedText(element)).toEqual('inside poof');
})));

it('should behave nicely if a mapped attribute is missing', async(inject(() {
var element = $('<parent-expression></parent-expression>');
$compile(element)(injector, element);
$rootScope.$apply(() {
$compile(element)(injector, element);
});

nextTurn();
expect(renderedText(element)).toEqual('inside ');
Expand All @@ -441,7 +446,9 @@ main() {
it('should behave nicely if a mapped attribute evals to null', async(inject(() {
$rootScope.val = null;
var element = $('<parent-expression fromParent=val></parent-expression>');
$compile(element)(injector, element);
$rootScope.$apply(() {
$compile(element)(injector, element);
});

nextTurn();
expect(renderedText(element)).toEqual('inside ');
Expand Down Expand Up @@ -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'<div><publish-me></publish-me></div>');
$compile(element)(injector, element);
$rootScope.$apply();
$rootScope.$apply(() {
$compile(element)(injector, element);
});

nextTurn();
expect(element.textWithShadow()).toEqual('WORKED');
Expand Down
66 changes: 66 additions & 0 deletions test/scope_spec.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "_specs.dart";
import "dart:async";

main() {
describe(r'Scope', () {
Expand Down Expand Up @@ -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;
Expand Down