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()
Related
I'm using angularJS 1.4.8, yesterday i noticed that the $scope.$watch doesn't trigger on every change which caused bug in my application.
is there a way to force it to work on every change immediately ?
like in this code, in every change on message i want the function in watch to trigger:
(function(){
angular.module('myApp', [])
.controller('myAppController', myAppController)
function myAppController($scope){
console.log('controller loading !');
$scope.message = 'message1';
$scope.$watch('message', function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
$scope.message='hi';
$scope.message='hi12';
}
changeMessage();
}
})();
the console will print:
controller loading !
newMessage hi22
plunker link https://plnkr.co/edit/SA1AcIVwr04uIUQFixAO?p=preview
edit:
I would really like to know if there are any other ways than wrapping the change with timeout and using scope apply, in my original code iv'e multiple places where i change the scope property and i would like to avoid using this every change.
This happens because the watch will only be triggered if the value is changed "between" digest loops.
Your function is changing the message value on the scope in the same function. This will be executed in the same digest loop.
When angular moves on to the next loop it will only see the last changed value which in your case will be hi22.
Here's a great article which makes this behaviour clear
update your changeMessage function so that it uses $scope.$apply function which will ensure that your changes are reflected and angular is aware of your changes to the variable.
changeMessage() {
setTimeout(function () {
$scope.$apply(function () {
$scope.message = "Timeout called!";
});
}, 2000);
}
If you change value into the same digest cycle the watcher is not triggered and last value is taken. When we run $timeout, we change $scope.message value in next digest cycle and watcher catches it as expected.
Take look on simple test:
$scope.$watch(function(){
console.log('trigger');
return $scope.message;
},
function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
$scope.message='hi';
$timeout(function(){
$scope.message='hi12';
});
}
Output:
controller loading !
trigger
newMessage hi
trigger
trigger
newMessage hi12
trigger
There is no need to wrap changeMessage in setTimeout and $apply at the same time. If you need to skip some time before execution, just use:
function changeMessage(){
$timeout(function(){
$scope.message = 'message';
}/* or add time here, doesn't matter */);
}
Or just:
function changeMessage(){
$scope.message = 'message';
$scope.$apply();
}
Both methods calls $rootScope.$digest in the end. Here is more information: https://www.codingeek.com/angularjs/angular-js-apply-timeout-digest-evalasync/
$watch() only triggers between every $digest().
Detailed explaination about the $apply() and $digest()
In your case you keep updating the $scope.message in the current $digest() cycle.
You could change that by applying each new value to the $scope using $apply(). Like #Ajinkya wrote.
The only problem, with setting 2000ms as timeout, doesn't allways ensure it executes after the $digest(). On top of that, Angular has a build in timeout function. See below.
(function(){
angular.module('myApp', [])
.controller('myAppController', myAppController)
function myAppController($scope, $timeout){
console.log('controller loading !');
$scope.message = 'message1';
$scope.$watch('message', function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
setTimeout(function () {
$scope.$apply(function () {
$scope.message='hi12';
});
}, 2000);
}
changeMessage();
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myAppController"></div>
Solution
The best way would be to call the build in $timeout function, without setting the time in milliseconds.
This way, angular allways ensures the $timeout will run after the latest $digest(). On top of that. You dont have to use the $scope.$apply(). Because the $timeout allready runs a $digest(), where $scope.$apply() is manually invoking a new $diggest() cycle.
(function(){
angular.module('myApp', [])
.controller('myAppController', myAppController)
function myAppController($scope, $timeout){
console.log('controller loading !');
$scope.message = 'message1';
$scope.$watch('message', function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
$timeout(function () {
$scope.message='hi12';
});
}
changeMessage();
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myAppController"></div>
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.
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);
I thought 2 ways binding was angular thing:
I can post from a form to the controller, and if I refresh the page I see my input on the page:
$scope.loadInput = function () {
$scope.me.getList('api') //Restangular - this part works
.then(function(userinput) {
$scope.Input = $scope.Input.concat(userinput);
// scope.input is being referenced by ng-repeater in the page
// so after here a refresh should be triggered.
},function(response){
/*#alon TODO: better error handling than console.log..*/
console.log('Error with response:', response.status);
});
};
In the html page the ng-repeater iterates over the array of for i in input. but the new input from the form isn't shown unless I refresh the page.
I'll be glad for help with this - thanks!
Try $scope.$apply:
$scope.loadInput = function () {
$scope.me.getList('api') //Restangular - this part works
.then(function(userinput) {
$scope.$apply(function(){ //let angular know the changes
$scope.Input = $scope.Input.concat(userinput);
});
},function(response){
/*#alon TODO: better error handling than console.log..*/
console.log('Error with response:', response.status);
});
};
The reason why: Your ajax is async, it will execute in the next turn, but at this time, you already leaves angular cycle. Angular is not aware of the changes, we have to use $scope.$apply here to enter angular cycle. This case is a little bit different from using services from angular like $http, when you use $http service and handle your response inside .success, angular is aware of the changes.
DEMO that does not work. You would notice that the first click does not refresh the view.
setTimeout(function(){
$scope.checkboxes = $scope.checkboxes.concat([{"text": "text10", checked:true}]);
},1);
DEMO that works by using $scope.$apply
setTimeout(function(){
$scope.$apply(function(){
$scope.checkboxes = $scope.checkboxes.concat([{"text": "text10", checked:true}]);
});
},1);