Please take a look at this code: (on plunker)
angular
.module('intervalExample', [])
.controller('ExampleController', ['$scope', '$interval', function($scope, $interval) {
$interval(function() {
//nothing..
}, 1000);
$scope.do = function() {
console.log(new Date());
return true;
}
}]);
<body ng-app="intervalExample">
<div>
<div ng-controller="ExampleController">
<div ng-if="do()">text</div>
</div>
</div>
</body>
If you open the console you'll see that $scope.do is being called every second. Why is that? why is $interval causing the directive to reload? Is there a way to avoid it?
It happens with other directives (such as ngShow) as well..
This is the entire point of how Angular works. Every time the handler registered via $interval fires, Angular does a digest cycle, which means it checks every watch expression to see if anything has changed. If a change is detected, Angular will modify the DOM to reflect the change. Because do() is called by a watch expression registered by ng-if, it is also called every time the timer fires.
If you don't want this, don't use $interval; just use a normal setInterval call, but note that any data you modify within the callback will not cause your Angular templates to be updated. This approach is not recommended. Rather, if the problem with calling do() every second is that do() is too expensive, find a way to make it cheaper, such as by caching the result.
Yes, I think this is because $interval causes digest pass and ng-if gets tested every time this happens, so your $scope.do() gets evaluated every time.
I don't think passing in a callable there is a good idea in general.
Related
Say there is an AngularJS controller like this:
var module = angular.module('myApp', []);
module.controller('TimeCtrl', function($scope, $interval) {
var tick = function() {
$scope.clock = Date.now();
window.scope = $scope;
}
tick();
$interval(tick, 1000);
});
Changing $scope.clock would automatically reflect in the DOM.
However, when I do it in the console, I need to do explicitly do
$scope.apply. Why is it so? Is $interval doing some magic?
In general, don't I need to $scope.watch these variables? I thought the purpose of $scope.watch was this.
1.
$interval(f, time)
is more or less
setInterval(function() {
f();
$rootScope.$apply();
}, time)
2.
<div>{{test}}</div>
is more or less
$scope.$watch('test', function(value) {
divElement.innerHTML = value;
})
Why is it so? Is $interval doing some magic?
Yes, it is. It is triggering automatically the AngularJS digest cycle, something you do "manually" with the $scope.$apply() (which also triggers it). This causes the changes to be reflected in the DOM. If the digest cycle is not triggered, AngularJS "does not know changes have been made in the model, so it does not update the DOM".
(...) don't I need to watch these variables?
No, unless you need to be notified when any of these variables have changed their values. As long as you do all changes inside the AngularJS scope the DOM will always be "notified" (updated).
How to know when I am doing the thing inside the AngularJS scope?
Usually when you use functions provided by services such as $interval and $timeout, you're doing things inside the scope, because these are wrappers of the original (setInterval and setTimeout) and trigger automatically the mentioned digest cycle, keeping things synced between model and DOM.
So, finally,
Why do we need $scope.apply() when doing a change from console but not
otherwise?
Because from console you are doing some changes outside the AngularJS scope and need to trigger the mentioned digest cycle by yourself.
Say we have the following simple form:
<form ngSubmit="doSomething()">
<input type="submit" value="Submit" />
<form>
And in the controller:
var ctrl = this;
ctrl.doSomething = function () {
// Anything can happen here
}
I know the digest cycle is triggered by ngSubmit, but how does Angular know when to run the digest cycle? Is it run after doSomething() is complete, and if so how does angular know that doSomething() has indeed completed because I haven't seen any examples of ngSubmit functions using a return statement? e.g.
ctrl.doSomething = function () {
// Anything can happen here
// Add return statement so that angular knows callback is complete perhaps?
return;
}
The reason I ask is because within my own doSomething() function I'm updating a scope value e.g. ctrl.myValue++. The updated value is being updated/reflected in the model fine but I don't understand how, or rather when, angular is able to ascertain that doSomething() has finished and it's now time to safely start the digest cycle.
I know the digest cycle is triggered by ngSubmit
Yes, ngSubmit directive like ngClick triggers digest cycle
I haven't seen any examples of ngSubmit functions using a return statement?
Please read more about Scope Life Cycle
"... assignment such as $scope.username="angular" will not immediately cause a $watch to be notified, instead the $watch notification is delayed until the $digest phase. This delay is desirable, since it coalesces multiple model updates into one $watch notification as well as guarantees that during the $watch notification no other $watches are running. If a $watch changes the value of the model, it will force additional $digest cycle."
So when you call ctrl.myValue++, the model changes and it fires digest cycle
Add return statement so that angular knows callback is complete perhaps?
No, I don't think so and this approach has not mentioned in Angular DOCs
Is this a bug in Angular or am I missing something:
Calling a function from the directive's template shows that that function is executed 11 times ! with a templateurl, and 22 times !! with a string template.
angular.module('testDirective', [])
.directive('myDirective', function() {
return {
scope:{},
template: '{{increment()}} {{count}}',
controller: function($scope) {
$scope.count = 0;
$scope.increment = function() {
$scope.count += 1;
};
}
};
})
HTML:
<body ng-app="testDirective">
<my-directive></my-directive>
</body>
RESULT:
22
Here is a Plunker with both template and templateUrl methods.
This is quite an issue when method calls are involved within repeaters as an example, this ends up calling the same method dramatically more time than it should.
Anybody could shed some light on this ?
{{increment()}} in your view is going to be called every time the digest cycle is called, like any bound expression would be. If you were incrementing in the controller function itself, rather than a bound function called from the view template, that would indicate that the directive was "loading" multiple times. But the nature of Angular is that any bound expression is going to be checked for changes repeatedly during the digest cycle, and anything you expect to happen only once (or even a predictable number of times) should never happen inside an expression bound to a content element. On the other hand, expressions bound to event handlers only execute when the event fires.
The model takes some time to get used to. Though the old documentation argued against it, you can use functions in content binding expressions, but they should never change state. You have no real control over when $digest() is called, and if you're thinking in the Angular way, you won't want to.
I also found this post which relates to the same issue to be useful :
Using ng-class with a function call - called multiple times
Also one-time binding can be a solution is some case as related here :
Angular lazy one-time binding for expressions
increment() is being called every time the view is being updated. On load, increment() is called, and the count goes up by 1 which causes the view to be updated and increment() to be called again.
If you look in your console, you will see that Angular is telling you that it's stuck in an infinite $digest loop.
I have this simple controller:
function MyCtrl($scope) {
$scope.value = 1;
$scope.getIncrementValue = function() {
return $scope.value + 1;
};
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<p>{{getIncrementValue()}}</p>
Which renders "2".
But as soon as I change to return $scope.value += 1; , I get some weird error and output in my browser is 23.
I could not find out why
Calling stateful, state-altering functions in your view interpolation is a very bad idea. You have basically no specific control over when or how often this function will be called.
View refreshes are triggered by many things in angular, so your function most likely actually gets executed 22 times.
You can check that by adding a console.log($scope.value) in your function, to see in the console how often it runs.
AngularJS analyzes your code and creates a structure of dependencies. Look at your first function that has return $scope.value + 1;. If anything would change the value of $scope.value, then this function would be executed again and the view would be updated.
Now look at return $scope.value+=1;. The function changes its own dependency, which means that it gets executed again, changes its own dependency, so its executed again, changes its own dependency... you get the picture.
AngularJS has a limit of such cycles, that's why it interrupts the process and the $scope.value is stuck at some arbitrary value.
This seems OK - Plunker
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.value = 1;
$scope.incrementValue = function()
{
$scope.value += 1;
};
});
Markup
<body ng-controller="MainCtrl">
<p>{{value}}</p>
<button ng-click="incrementValue()">Increment</button>
</body>
No need to return.
Angular have Digest cycle that call any time and it verify any change in scope variables and It also call the functions you have bind.
Here are the steps as per you functionality.
First digest call function getIncrementValue().
getIncrementValue() change the value that is in scope.
Digest again called so updated value can update if that is using some other place also. AND HERE IT ALSO CHANGE THE VALUE.
So it is infinite loop.
See in the below example. I am updating another value and on every change your function also called :)
[http://plnkr.co/edit/CVX2IXErS4hIT7fLfzBx?p=preview][1]
I konw that $apply used to connect the Javascript context and the AngularJS context.
A simple example is below:
template:
<div>{{someVal}}</div>
javascript in controller:
setTimeout(function() {
scope.$apply(function(){scope.someVal = 123});
}, 1000);
We need use $apply in above situation.
First Qustion:
If I modify javascript above to:
setTimeout(function() {
scope.someVal = 123;
}, 1000);
scope.$watch('someVal', function(val) {
console.info(someVal);
});
No console about someVal modified to 123... Why? Can't we watch expression modified in timeout callback?
Second Question:
If we use ngSwitch directive like below:
<div ng-switch on="sub">
<div ng-switch-when="a">
//state a
</div>
<div ng-switch-when="b">
//state b
</div>
</div>
When I modify the sub in controller:
scope.sub = 'a';
setTimeout(function() {
scope.sub = 'b';
}, 1000);
No need to use $apply!!!! Why?
I found that ngSwitch directive use $watch to monitor on attribute value. Why ngSwitch can watch scope attribute modified in timeout callback?????
Pls tell me the reason about the 2 qustions above.
From AngularJs documentation
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
window.setTimeout is a JavaScript function, so whatever you use setTimeout you must use $apply() to update the model.
Your second example wouldn't work without $apply(), I've made a demo to clarify the $watch and $apply issue. Please check it.
You can use $timeout that is a wrapper for window.setTimeout and also this way you wont need to use $apply on the callback:
$timeout(function() {
scope.someVal = 123; }, 1000);
When you run code that is outside Angular, you'll need a way to let the Angular and the watchers of that value, to know that it has changed. Thats what $apply is for, it will allow watch listeners to fire
About you second question, of why the scope is updating without a $apply, you should be firing indirectly somehow a $apply/$digest. To give you a more specific answer, a Plunker will be necessary to check what else in going on on your code.
Use the Angularjs $timeout service instead of setTimeout and you will not require $apply. Same thing is if you are using a jquery http call, use the angular http service to avoid using $apply.