I was converting a DurandalJs (using BreezeJs for the async calls) application to AngularJs, the team decided to keep using BreeseJs with AngularJs, so I started notice that when I update a $scope variable (to update the view) the change wasn’t reflecting in the view.
I realized that the problem was that when you try to update an $scope variable when a promise come back, this operation is done outside the angular digest cycle, and that’s why it was not updating the view.
This only happens when you deffer a call outside Angular (you can prevent this using the $http and $q services).
The first solution I came across, was to call $scope.$apply(). But I start receiving this error:
Error: $digest already in progress
It means your $scope.$apply() isn't high enough in the call stack (digest cycle).
I kept looking and I found this code.
if(!$scope.$$phase) { //$digest or $apply }
It looks a little hacky, well, it is!
It is considered an Anti-Pattern (2- Don't do if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack.):
https://github.com/angular/angular.js/wiki/Anti-Patterns
I thought to myself, there should be a way to do this, and I found this, this is the solution:
$timeout(function(){ //this code is included in the digest cycle })
Also there is another safe option if you have underscore (this is not the best practice either):
_.defer(function(){$scope.$apply();});
Things you should know:
- $$phase is indeed private to the framework and there are good reasons for that
- $timeout(callback) will wait until the current digest cycle (if any) is done, then execute your code, then run at the end a full $apply
- $timeout(callback, delay, false)will do the same (with an optional delay before executing your code), but won't fire an $apply which saves performances if you didn't modify your model
- $apply invokes, among other things, $root.$digest, which means it will redigest the root scope and all of its children, even if you're within an isolated scope.
- $digest will simply sync its scope model to view, but won't tell its parents scope, which can save a lot of performances when working on an isolated part of your HTML with an isolated scope (from a directive)
- $evalAsync has been introduced with angularjs 1.2, that will probably solve most of your troubles, please refer to the last paragraph to learn more about it
- if you get the "$digest already in progress" error, then your architecture is wrong: either you don't need to re-digest your scope, or you should not be in charge of that.
Here is a jsFiddle working example.
Please feel free to leave a comment with questions. 😉