When is angularjs `$digest` triggered? - javascript

I am trying to implement a function to check for user inactivity. My online search has suggested that I can use the $rootScope.$watch to monitor activity. However, I am a bit concerned about the $digest trigger... as I can't seem to be able to find exactly when or which events trigger this. Any pointers will be appreciated.

Angular's $digest alone isn't going to solve the requirement, in my opinion. What if the user moves their mouse, shouldn't that restart the idle timer?
What I recommend is making a directive that creates an event listener on the $window object for both keydown and mousemove. Then create a service that will allow other components to receive notification when the user's idle state changes.
Something like this:
angular
.module('myApp', [])
.service('Idle', function(){
var self = {
callbacks: {
onidle: []
},
on: function(event, callback){
event = 'on' + event.toLowerCase();
if(self.callbacks[event] && typeof callback === 'function'){
self.callbacks[event].push(callback);
}
return this;
},
trigger: function(event){
event = 'on' + event.toLowerCase();
if(self.callbacks[event])
{
lodash.each(self.callbacks[event], function(callback){
callback.call();
});
}
return this;
}
}
return {
on: self.on,
trigger: self.trigger
}
})
.directive('idleNotification',
function(
$window,
Idle
){
return {
link: function($scope){
var timer = setTimer();
angular
.element($window)
.on('keydown mousemove', function(){
clearTimeout(timer);
timer = setTimer();
});
function setTimer(){
return setTimeout(function(){
Idle.trigger('idle');
}, 30);
}
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Then use the Idle service like so:
Idle.on('idle', function(){
// do something here when the user becomes idle
});

Angular triggers $digest (generally) all by itself. Your $watch should "just work".
Oh - if you really need to initiate the $digest loop yourself, you should use $scope.apply() instead of calling digest directly.
What really happens behind the scenes though is all kinds of things actually trigger a $digest, including ng-click events, timeouts, watchers, long-polling, websockets, http calls, promises - anything that changes the internal application state that angular manages (via "dirty checking"). For more info, see here: https://www.sitepoint.com/understanding-angulars-apply-digest/ and https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest

To understand the Angular context, we need to look at exactly what
goes on inside of it. There are two major components of the $digest
loop:
When Angular thinks that the value could change, it will call $digest() on the value to check whether the value is “dirty.” Hence, when the Angular runtime is running, it will look for potential changes on the value.
$watch lists are resolved in the $digest loop through a process called dirty checking, so after the scope expression is evaluated and the $digest loop runs, the $scope’s watch expressions will run dirty checking.
During the $digest cycle, which executes on the $rootScope, all of the children scopes will perform dirty digest checking. All of the watching expressions are checked for any changes, and the scope calls the listener callback when they are changed.
For example
When we call the $http method, it won’t actually execute until the next $digest loop runs. Although most of the time we’ll be calling $http inside of an $apply block, we can also execute the method outside of the Angular digest loop.
To execute an $http function outside of the $digest loop, we need to wrap it in an $apply block.
That will force the digest loop to run, and our promises will resolve as we expect them to.
$scope.$apply(function() {
$http({
method: 'GET',
url: '/api/dataFile.json'
});
});
Hope this much understanding help!

Related

How do $digest loop invoked by angular when an event fires?

I was trying to understand the in and out of angularjs.
I come across $digest loop, $watch lists etc, however my doubt is when an event fires at browser (Button click) how do angular knows it? In javascript if we subscribed to an event then, we get notification when the event fires. How do angular get those notification to invoke the $digest loop.(Please note that I am not trying to understand about ng-click directive.)
I tried google it, but I did not get the details. Any URLs or book name is just fine.
Thank you.
From Angular docs about Scope Life Cycle:
When the browser calls into JavaScript the code executes outside the
Angular execution context, which means that Angular is unaware of
model modifications. To properly process model modifications the
execution has to enter the Angular execution context using the $apply
method. Only model modifications which execute inside the $apply
method will be properly accounted for by Angular. For example if a
directive listens on DOM events, such as ng-click it must evaluate the
expression inside the $apply method.
To make sure Angular evaluates the $scope modifications, you need to use the built-in in handlers: ng-click, $timeout, etc. After executing the handler, Angular will make sure to perform a dirty check and update the DOM if necessary. The $scope.$apply() is called automatically.
In other cases, when using handlers not related with Angular: setTimeout(), document.body.addEventListener(), etc. the developer should take care by himself about dirty check, by calling manually $scope.$apply() in the handler.
For example:
1) Automatic dirty check by Angular (the recommended and correct way)
angular.module('App').controller('Ctrl', function($timeout) {
$scope.myText = 'Initial text';
$timeout(function() {
$scope.myText = 'Initial text after 1 second';
//$scope.$apply() is called automatically
}, 1000);
});
2) Manually evaluate the scope changes
angular.module('App').controller('Ctrl', function($timeout) {
$scope.myText = 'Initial text';
setTimeout(function() {
$scope.myText = 'Initial text after 1 second';
$scope.$apply(); // should be called manually
}, 1000);
});
The magic here generally happens in the ng-click directive (or one if it's related directives like ng-mouseenter, ng-mouseleave, ng-keypress, etc.)
Looking at the source, the directive simply registers a callback for the appropriate event using angular's JQLite (i.e. element.on(event, callback)). The callback itself executes the expression in the correct scope via $scope.$apply (which starts the digest loop).
If you are going to register your own event listeners outside of the normal angular directives, you'll want to follow the same pattern they use.
The source code is remarkably simple to read and is worth having a look at.

Does the angular $interval cancel itself after exceeding 'count' parameter?

Quick question about the $interval service in angular. Looking at the docs ($interval) they warn you to manually cancel intervals, but you have the option of providing a count parameter upon initialization. Once the timer has "ticked" past the allotted count does it cancel itself or simply stop calling the function and live on in the background?
TL;DR; After the count, the interval is cleared.
As the same documentation says, it is recommended that you cancel the $interval when the scope of your controller is detroyed. Something like:
var t = $interval(function(){
...
}, 1000);
$scope.$on('$destroy', function(){
$interval.cancel(t);
});
The delay parameter is time interval which the function is called. In the example above, the function is called every 1000 milliseconds. If you don't cancel the $interval, Angular will hold a reference to it, and may continue to execute your function, causing strange behaviors in your app.
Considering that the $interval provider is just a wrapper of the native setInterval(), with the adition of the $apply, looking at the Angular implementation (https://github.com/angular/angular.js/blob/master/src/ng/interval.js), we can find this snippet of code:
if (count > 0 && iteration >= count) {
deferred.resolve(iteration);
clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
}
So, the promise created by the provider is resolved, and the interval is cleared. The cancel method does this:
intervals[promise.$$intervalId].reject('canceled');
$window.clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
So, I think your assumption is right. After the count, the interval is already cleared.

How to update elements that are bound to functions with Angular?

What is the correct way to do have elements update regularly, without explicitly doing that for each variable in an $interval callback?
This would be useful in the following situation: an element's content is bound to a function, like this:
<p ng-bind="timeLeft()"></p>
The timeleft() function returns a value depending on the current time. It needs to be refreshed every 20 ms.
Edit: usually ngBind updates just fine, but I guess because the outcome of timeLeft() only depends on time passing, in this case it doesn't. Without the code below, the view is not updated after loading the DOM, at least not without updating other values.
I've tried the following, which works:
$scope.updateView = function(){
if(!$scope.$$phase) { // To prevent this: "Error: [$rootScope:inprog] $digest already in progress"
$scope.apply();
}
}
$interval( function(){
$scope.updateView();
}, 50);
I noticed just using $interval may already suffice, in my setup this works too:
$scope.updateView = function(){
}
$interval( function(){
$scope.updateView();
}, 20);
Would it be reliable to use that last solution?
Yes, you should be reliable with second solution.
Angular does not run the digest cycle when event occurred from outside angular context, at that you need to tell AngularJs to run apply cycle to update all bindings. When you are using $interval angular will run digest cycle after an execution of callback function, there is no need of running digest cycle which you have done in your 1st solution.
You need to look at $interval service
$interval($scope.updateView,
20, //delay
10, //this option will run it 10 times, if not specified then infinite times
true, //do you want to run digest cycle after callback execution, defaulted to true
{} //any extra params
);

Compare ways of causing digest

There is this article I saw long time ago:
https://coderwall.com/p/ngisma
It describes a method that triggers $apply if we are not in an apply or digest phase.
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
Angular has the $scope.$evalAsync method (taken from 1.2.14):
$evalAsync: function(expr) {
// if we are outside of an $digest loop and this is the first time we are scheduling async
// task also schedule async auto-flush
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
}
Which calls digest if we are not in a phase and adds the current invocation to the asyncQueue.
There is also the $apply, $digest and $timeout methods.
It is confusing.
What is the difference between all ways mentioned to trigger a digest cycle (a dirty check and data binding)?
What is the use case for each method?
Is safeApply() still safe? :)
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?
As a high level introduction I would say that it is rarely required to actually initiate your own digest cycle, since angular handles most cases.
That being said let's dive into the question.
As a very high level, the $digest loop looks like this:
Do:
- - - If asyncQueue.length, flush asyncQueue.
- - - Trigger all $watch handlers.
- - - Check for "too many" $digest iterations.
While: ( Dirty data || asyncQueue.length )
So basically $evalAsync is adding the function to the asyncQueue and defering a digest if it needs to. However if it's already in a digest cycle it will flush the asyncQueue and it will just call the function.
You may notice that this is very similar to the safeApply. One difference is that instead of adding the function to the asyncQueue it just calls it, which can happen in the middle of a cycle for instance. The other difference is that it exposes and relies on a $$ variable, which are intended to be internal.
The most important difference however between $evalAsync and $apply from $digest (I will get to $timeout below) is that $evalAsync and $apply starts the digest at the $rootScope but you call the $digest on any scope. You will need to evaluate your individual case if you think you need this flexibility.
Using $timeout is very similar to $evalAsync again except that it will always defer it out of the current digest cycle, if there is one.
$timeout(function() {
console.log( "$timeout 1" );
});
$scope.$evalAsync(function( $scope ) {
console.log( "$evalAsync" );
});
If you already in a digest cycle will give you
$evalAsync
$timeout 1
Even though they are called in the opposite order, since the timeout one is delayed to the next digest cycle, which it instantiates.
EDIT For the questions in the comment.
The biggest difference between $apply and $evalAsync as far as I can tell is that $apply is actually triggering the $digest cycle. To you all this means is that you need to be sure you call $apply when you aren't in a digest cycle. Just for transparency, the following code is also valid:
$scope.$apply(function() {
$scope.$evalAsync(function() {
});
});
Which will call the apply function, add the no-op function to the $asyncQueue and begin the digest cycle. The docs say it is recommended to use $apply whenever the model changes, which could happen in your $evalAsync
The difference between fn(); $scope.apply() and $scope.apply(fn) is just that the $scope.apply(fn) does a try catch for the function and explicitly uses $exceptionHandler. Additionally, you could actually attempt to cause digest cycles in fn, which will change how the apply is handled.
I'd also like to point out at this time that it is even more complicated in 1.3 assuming it stays this way. There will be a function called $applyAsync used to defer apply calls (reference).
This post compiled some information from This blog and this so post plus some of my experience.
Hope this helped!
There's only one way to start a digest cycle: Calling $digest. It's rarely called directly.
$apply is essentially a safe wrapper for $digest. Safe means that it handles errors. So most of the time this is the method you want to call to propagate changes. Another important difference is that it always starts a digest cycle at the root scope, whereas $digest can be called on any scope and only affects that scope and its descendants.
Many services and directives trigger a digest cycle, usually by calling $apply. The only important thing to know is wether you have to call $apply yourself or it has already been called by the service / directive.
Is safeApply still safe?
Yes, because there's still only one way to start a digest cycle.
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?
Not calling $apply in the middle of a digest cycle. Seriously. Being in that situation is almost always a design issue in the first place. It's better to resolve that issue instead of covering it.
Anyway the method used by the solution, checking $$phase, is the only way of knowing if a digest cycle is running. As shown by you it's even used by Angular internally. It's enough to do if (!$rootScope.$$phase). But keep in mind that it's a hack that might break with a new version of Angular. BTW: Monkey patching the topmost scope, as proposed by the author, would make the solution useless for isolated scopes.
Angular's way of data binding might indeed be difficult to understand for someone new to Angular at first. But basically it all boils down to this: For every change to be recognized / processed someone has to call $apply (or $digestfor that matter). That's it. This is supposed be done by directives and services. It's actually quite hard to get a '$apply already in progress' error if you have done everything right.

Why doesn't Angular ignore subsequent $digest invocations?

The following must be the most common error Angular.js developers have to troubleshoot at some point or another.
Error: $apply already in progress
Sometimes, a $scope.$apply call wasn't required after all, and removing it fixes the issue. Some other times, it might be needed, and so developers resort to the following pattern.
if(!$scope.$$phase) {
$scope.$apply();
}
My question is, why doesn't Angular merely check for $scope.$$phase and return without doing anything when a $digest loop is in invoked, or when $scope.$apply is called, rather than complain and throw an error so insidiously hard to track down.
Couldn't Angular just check for $$phase itself at the entry points of any methods which trigger a $digest and save the consumers from having to go through the trouble?
Are there any implications in leaving misplaced $scope.$apply calls around, other than unwanted watches firing, which the check for $$phase would prevent?
Why isn't a$$phase check the default, rather than an oddity?
Background
I'm upgrading Angular from 1.2.1 to 1.2.6 and that exception is being thrown when the page loads, so I have no idea where it's originating from. Hence the question, why is it that we even have to track down this obscure sort of bug with no idea of where in our code to look for it?
If you ever get this error, then you're using $scope.$apply() in the wrong place. I think the reasoning behind this error is as follows: if you're calling $apply in the wrong place, you probably don't fully understand the reason it exists, and you may have missed places where it actually should be called.
If such lapses are allowed to go by silently (by always testing $scope.$$phase first, for example), it would encourage sloppy usage, and people might get holes in their "$apply coverage". That kind of bug (where a $scope change is not reflected in the view) is a lot harder to track down than this error message, which you should be able to debug by examining the stack-trace (see below).
Why does $apply exist?
The purpose of $apply is to enable automated two-way data binding between $scope and view, one of the main features of Angular. This feature requires that all code that may potentially contain a $scope modification (so, basically every piece of user-code) is run below an $apply call in the call-stack. Doing this properly is actually not that complicated, but I think it's not documented very well.
The main 'problem' is that Javascript can have any number of active call-stacks, and Angular is not automatically notified about new ones. A new call-stack is created whenever an asynchronous callback is triggered, e.g., from a click, a timeout, a file-access, a network response, and so on. It's very common to want to modify the $scope inside such a callback function. If the corresponding event was provided by Angular itself, everything will just work. But there are times when you'll want to subscribe to 'outside events'. Google Maps events, for example:
function Controller($scope) {
$scope.coordinates = [];
//...
var map = new google.maps.Map(mapElement, mapOptions);
google.maps.event.addDomListener(map, 'dblclick', function (mouseEvent) {
$scope.coordinates.push(mouseEvent.latLng);
});
}
http://jsfiddle.net/mhelvens/XLPY9/1/
A double-click on the map of this example will not trigger an update to the view because the coordinates are added to $scope.coordinates from an '$applyless' call-stack. In other words, Angular does not know about Google Maps events.
How and where to use $apply?
We can inform Angular about the event by using $scope.$apply():
function Controller($scope) {
//...
google.maps.event.addDomListener(map, 'dblclick', function (mouseEvent) {
$scope.$apply(function () {
$scope.coordinates.push(mouseEvent.latLng);
});
});
}
http://jsfiddle.net/mhelvens/XLPY9/2/
The rule is to do this first thing inside every callback function for an outside event. The "$apply already in progress" error is an indication that you're not following this rule. If you have to handle Google Maps events often, it makes sense to wrap this boilerplate code in a service:
app.factory('onGoogleMapsEvent', function ($rootScope) {
return function (element, event, callback) {
google.maps.event.addDomListener(element, event, function (e) {
$rootScope.$apply(function () { callback(e); });
});
};
});
function Controller($scope, onGoogleMapsEvent) {
//...
onGoogleMapsEvent(map, 'dblclick', function (mouseEvent) {
$scope.coordinates.push(mouseEvent.latLng);
});
}
http://jsfiddle.net/mhelvens/XLPY9/3/
The onGoogleMapsEvent events are now Angular-aware, or 'inside events' if you will (I'm totally making these terms up, by the way). This makes your code more readable and allows you to forget about $apply in your day-to-day programming; just call the wrapper instead of the original event subscriber.
For a number of events, Angular has already done this for us. With the $timeout service, for example.
Debugging the "$apply already in progress" error
So let's say I use the wrapper, but, absentminded as I am, I still call $apply manually:
onGoogleMapsEvent(map, 'dblclick', function (mouseEvent) {
$scope.$apply(function () { // Error: $apply already in progress
$scope.coordinates.push(mouseEvent.latLng);
});
});
http://jsfiddle.net/mhelvens/XLPY9/4/
This $apply call does not follow our placement rule (onGoogleMapsEvent events are not outside events, after all; we granted them 'insideness'). If you double-click on the map, you'll see the error appear in the logs, together with a stack-trace:
Error: [$rootScope:inprog] $apply already in progress
...
at Scope.$apply (.../angular.js:11675:11)
at http://fiddle.jshell.net/mhelvens/XLPY9/4/show/:50:16
...
at Scope.$apply (.../angular.js:11676:23)
at dl.ondblclick (http://fiddle.jshell.net/mhelvens/XLPY9/4/show/:33:24)
...
I've left only the relevant lines: the ones that refer to Scope.$apply. When you get this error message, you should always find two Scope.$apply calls on the stack (or Scope.$digest calls, which serve a similar function; $digest is called by $apply).
The topmost call indicates where we got the error. The other one indicates why. Go ahead and run the Fiddle with your Javascript console opened up. In this case we can conclude: "oh, this was an 'inside event' subscriber, I can just remove my manual $apply call". But in other situations you may find out that the call lower down on the stack is also not positioned properly.
And that's precisely why I believe the error is useful. Ideally you'd only get an error when you neglect to call $apply altogether. But if Angular could do that, there would be no need to call the function manually in the first place.

Categories