I've been looking into the caveats of the AngularJS Digest cycle and I would like to better understand where the separation is between using it properly and improperly.
For example, if I have an AngularJS code that looks like this:
var myApp = angular.module("testApp", []);
myApp.controller("testController", ["$scope", "$timeout", function($scope, $timeout){
setTimeout(function(){
$scope.username = "Test User name";
}, 3000);
$timeout(function(){
$scope.username = "AngularJS User name";
}, 3000);
}]);
Why is setTimeout not being used as part of the Digest Cycle, whereas $timeout is, and how can I fix this to work?
Please keep in mind, I am looking not only for a code solution but for an explanation of why this occurs. As good as a code solution may come, it will not explain itself.
$timeout is an angularized version of setTimeout(), i.e. it is coded in such a way that it triggers a digest cycle. setTimeout() is a plain Javascript function that knows nothing about Angular or a digest cycle. Since setTimeout() is not a simple JS object, Angular cannot $watch it.
So the whole point of having functionality like $timeout is that they are angular-friendly versions of some Javascript functionality.
$timeout() and setTimeout() are not the same, $timeout is angularised.
It uses promise ($q service) internally, which would resolve after every digest cycle, automatically. Whereas setTimeout(), is just a trick with registering your callback function in the queue.
If you want setTimeout() to be part of digest loop, call it within the $scope.$apply():
setTimeout(function(){
$scope.$apply(function(){
$scope.username = "Test User name";
});
}, 3000));
A more generic explanation is that setTimeout does not work inside of angular because it puts its callback on the event loop, which angular does not watch. The same situation would happen if you were to create an XMLHttpRequest directly instead of $http.
Angular has made their own abstraction of these utilities/objects so that when they finish, the digest cycle will pick up any changes.
Related
I have to bind js event on a element after it have been shown by changing a $scope variable using ng-show.
After I change the scope I run JS script but the Js is fastest than angular that he miss found some div not shown yet by ng-show.
Here is a plunker for some code sample to this problem. The solution I found for this kind of problem is to do the JS code in a setTimeout but I need a clean solution : https://plnkr.co/edit/iYDs552yCyd3R7ivgD84?p=preview
$scope.start = function(){
$scope.showMap=true;
setTimeout(function(){
jsFunc();
},1000);
}
Use the $timeout service:
$scope.start = function(){
$scope.showMap=true;
/*
setTimeout(function(){
jsFunc();
},1000);
*/
//Use $timeout
$timeout(jsFunc);
}
The $timeout service is integrated with the AngularJS digest cycle.
Since the Leaflet API is external to the AngularJS framework, a $timeout is necessary for the browser to render the DOM and execute the Leaflet API code.
For more information, see AngularJS $timeout Service API Reference.
I get a problem with follwoing code.
my html page code :
<body ng-app="myapp">
<div ng-controller="myController">
The message is {{message}}
<input type="button" value="Change Message" ng-click="changeMessage()">
</div>
My Controller code:
app.controller('myController',function($scope)
{
$scope.changeMessage=function()
{
setTimeout(function(){
console.log("Message changed");
$scope.message="Hurray !!! New Message";
},3000);
$scope.newMessage=function()
{
$scope.message="hello";
console.log("new message");
};
But if I use changeMessage function I am not able to see the changed Message property even though the console.log message comes.
what is missing here in both cases.
Thanks in advance
The reason that the change isn't reflected in the view is that because of the assignment is done in the callback in setTimeout which results in angular not noticing the change. This has to do with the so called digest cycle. There are different ways to solve this.
use $scope.$apply() to wrap your assignment
or even better use the existing $timeout service provided by angular instead of the call to setTimeout which already handles the problem above for you.
For details, see https://docs.angularjs.org/api/ng/service/$timeout for usage of $timeout
and https://docs.angularjs.org/api/ng/type/$rootScope.Scope for the reasons behind $apply.
A general explanation of what is happening here is described at http://www.sitepoint.com/understanding-angulars-apply-digest/
every change should happen within an angular-digest-cycle. if you change values from the outside (which is exactly what happens, if you use setTimeout instead of angular's $timeout) angular does not update your view until the next digest-cycle (https://www.ng-book.com/p/The-Digest-Loop-and-apply/). so in your case the message is already set, but the view has not been updated.
try something like this:
app.controller('myController', function($scope, $timeout) {
$scope.changeMessage = function() {
$timeout(function(){
console.log("Message changed");
$scope.message="Hurray !!! New Message";
}, 3000);
$scope.newMessage=function() {
$scope.message="hello";
console.log("new message");
};
you should use $digest() after that the timeout is done:
$scope.changeMessage=function()
{
setTimeout(function(){
$scope.message="Hurray !!! New Message";
$scope.$digest();
},3000);
}
please note that it is much better to use $digest instead of $apply (performance related):
scope.$digest() will fire watchers on the current scope, and on all of its children, too. scope.$apply will evaluate passed function and run $rootScope.$digest()
I am reading AngularJS in Action by Lukas Ruebbelke to clear the concept of dirty checking as to how AngularJS works at a molecular level.
The author puts forward,
It is during the digest cycle that all watch expressions for a scope object
are evaluated. When a watch expression detects that a $scope property has
changed, then a listener function is fired.
Ocassionally a property is changed without AngularJS knowing about it. You
can manually kickstart a digest cycle vis $apply.
So, my question is what are those situations in a real web application when I need to kick off this digest cycle manually. And are those situations often seen? Kindly suggest.
This will come up any time an asynchronous callback returns from a non-angular library. e.g.
setTimeout(function() {
$scope.myVar = 1;
//Angular doesn't know when setTimeout finishes
//so you have to manually kick off a digest cycle.
$scope.$apply();
});
Angular has the $timeout service which takes care of starting a digest cycle for you but if you are using some third party library that takes a callback and doesn't have an angular wrapper then you will have to do this.
These situations can happen when using 3rd party libraries which provide some kind of data for example.
Say you use library-X which fires an event when something happened and new data is available, which you would like to render with AngularJS.
In these causes AngularJS does not know that data in the scope changed if you just directly set the variables.
That is why you should only modify scope variables inside the $apply function:
function MyController($scope) {
$scope.load = function() {
$scope.message = 'Loading...';
setTimeout(function() {
$scope.$apply(function () {
$scope.message = 'Finished loading!';
});
}, 2000);
}
}
It is also advised to use $scope.$apply(function () { /* update code */ }) instead of the single $scope.$apply() call, since it will properly catch errors and run the diggest regardless of any errors.
I'm trying to understand how to properly manipulate properties via a controller. The following code executes six updates over four seconds. Updates two and three are not reflected in the view. Why is this, and what do I need to do to have updates of those types affect the view?
Html
<div ng-controller="Controller">
myValue: <span ng-bind="myValue"></span>
</div>
Javascript
var app = angular.module('myApp', []);
app.controller('Controller', function ($scope, $interval) {
$scope.myValue = "first";
console.log($scope.myValue);
setTimeout(function() {
$scope.myValue = "second"; // never updates
console.log($scope.myValue);
$scope.$emit("my-event", "third"); // never updates
console.log($scope.myValue);
$interval(function() {
$scope.$emit('my-event', "fourth");
}, 1000, 1);
}, 1000);
$interval(function() {
$scope.myValue = "fifth";
console.log($scope.myValue);
$interval(function() {
$scope.$emit("my-event", "sixth");
}, 1000, 1);
}, 3000, 1);
$scope.$on('my-event', function (event, arg) {
$scope.myValue = arg;
console.log(arg);
});
});
JSFiddle
Use $timeout instead of setTimeout to opt-in to the digest cycle. second won't show since the turn of the digest cycle overrides the value of myValue.
Updated fiddle: https://jsfiddle.net/d9gbpddy/4/
You can try {{myValue}} instead of a <span> element
So I obviously wasn't clear enough in the original question, as the upvoted answer (correctly) suggests using $timeout rather than setTimeout, however the original intent was to understand why the updates were not being reflected in the view, and what could be done to have these types of updates (that originate outside angular) affect the view as was intended.
Read the Scope guide
So whilst I chose to skip the Scopes section of the developer guide because it looked to be the most boring, it was probably the most important, and it clearly points out some items imperative to understanding how angular binds data, notably the Scope Life Cycle which notes;
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.
There's an excellent answer here that further explains this concept. The first setence aptly reiterates the importance of understanding scope:
You need to be aware about how Angular works in order to understand
it.
Don't just call $scope.$apply
So you start adding calls to $scope.$apply around the place to cater for these things that originate outside angular, but then eventually you start getting:
Error: $digest already in progress
Which means you can't call $scope.$apply whilst $digest is executing. After which you may think, well how can I conditionally call $scope.$apply based on whether the $digest is currently running. But, you don't need to do that...
Just use $timeout
Hah, like the upvoted answer, I know, but based on a different thought process I think. See this answer. $timeout is not just being used in place of setTimeout, but rather is being used (without a delay) to wrap any model updates that are called from outside the Scope Life Cycle, and doing so ensures no conflict with any currently processing $digest.
Wrapping up
So, in the original code snippet, the second and third updates are not reflected in the view because they are performed outside the Angular execution context. The fact that third update doesn't affect the model also means that calling events outside the execution context doesn't get you into the execution context either.
The fourth update is already wrapped inside $interval, which itself causes the updates that code to be run on the next digest. Therefore, updating the code to show an example of an event outside the angular execution context that causes its updates to be shown in the view is as follows:
setTimeout(function() {
$timeout(function({ // start wrap
$scope.myValue = "second"; // now this updates!
console.log($scope.myValue);
$scope.$emit("my-event", "third"); // now this updates!
})); // end wrap
console.log($scope.myValue);
$interval(function() {
$scope.$emit('my-event', "fourth");
}, 1000, 1);
}, 1000);
On the remarkably brief AngularJS $timeout documentation page, the 'delay' argument is stated as optional. When using $timeout without specifying a delay, I note that a delay is still applied.
Can anyone tell me how much time is allotted for the delay when the argument is left implicit?
When $timeout delay is omitted, it defaults to 0. However, the block of code contained in it is executed after the DOM has been manipulated by Angular. See response to AngularJS $evalAsync vs $timeout
My understanding is that a delay of '0' means that it will be picked-up as part of the next run of the event loop. That's an especially short but indeterminate amount of time.
It's immediately executed, the default would be zero. Here is a jsfiddle showing it:
http://jsfiddle.net/dgarlitt/rqs3p/1/
angular
.module('myApp',[])
.controller('MyCtrl', function($scope, $timeout) {
$timeout(function() {
$scope.name = 'World';
});
});
The default delay is 0. The documentation has been updated since.
offical angularjs $timeout doc