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]
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.
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.
Say we have this expression
$scope.showButton = users.is_admin() && !document.is_provided;
And then in the same controller you have a button that updates the value of Document.is_provided:
<button ng-click="document.is_provided = true;">Provide document</button>
The problem is that $scope.showButton should now changed but it's not changing.
Updated:
Plnkr showing simplified issue: http://plnkr.co/edit/Qk2jnHAmqsrNXSAjDYEk?p=preview
I'd like to add on to squiroid's answer and explain to you why it works. I'll provide a very simple way for you to understand, as this was a very common problem for me too when I started.
Angular has something called a Digest cycle. It is a series of functions that are called successively to make sure that if a variable A is two-way bound to another variable B (using ng-model for example), they always stay in sync (that is, always have the same value). Functions that angular provides (or services, that usually start with $, like $timeout etc), automatically start the digest cycle
So let's say that you have a variable A and you've bound it to the $scope variable B, which is equal to a variable C. You would expect C to be bound to A too, right? Because you think when you change C, B should change, and so, A should change too. But that is only possible when you start the digest cycle. The cycle will look for variables that need to be updated, and will update them. However, by simply changing the value of C, you will never trigger the digest cycle, and so your change will never propagate to A.
What you do when you put a variable in $watch is (as the $ suggests), you forcefully start a digest cycle. So now when you change C, you start the cycle, it tracks down that it needs to update A too, and that's why it works.
In your example, $scope.showButton is bound to document.is_provided. Now you're using a non-angular function to change the value of document.is_provided. This means that you do not trigger the digest cycle, and that means your change will never be propagated to $scope.showButton. Therefore, showButton always remains false.
And of course, an example to help you understand:
http://jsbin.com/vojoso/edit?js,console,output
Watch the console. I've added logs to help you understand.
Comment out the $watch statement and see what happens.
Try understanding it and then doing it yourself, you'll get it.
Hope I helped :)
I am not sure but you can watch on it:-
$scope.$watch(function(){
return Users.is_admin() && !Document.is_provided;
},fuction(newVal){
$scope.showButton =newVal;
});
Hope it helps :)
Here is good article about $scope.$watch, it should help to understand how to solve your problem.
It you need bigger answer add code which will explain Document variable, and your REST service.
UPD: I see you changed you original question. I suppose you have trouble with controller. Try do not use $scope implicitly, use data attribute as sad angular code style
Also show the part in you template where you connect controller.
UPD 2: You have some misunderstanding of my words, so I modified your plunker example
index.html
<html ng-app="plunker">
<body ng-controller="MainController as main">
<div ng-show="main.data.document.is_provided">visible</div>
<button ng-click="main.hide()">Hide</button>
<script data-require="angular.js#1.4.x"
src="https://code.angularjs.org/1.4.1/angular.js"
data-semver="1.4.1"></script>
<script src="app.js"></script>
</body>
</html>
app.js
angular
.module('plunker', [])
.controller('MainController', MainController);
function MainController() {
var main = this;
this.data = {
document: {
is_provided: true
}
};
this.hide = hide;
function hide() {
main.data.document.is_provided = false;
}
}
You have one problem inside ng-click, as there should be function execution. Also you are using ng-if instead of ng-show.
The best way (imho) to deal with this kind of problems:
Use {{ anyObject | json }} in your view!
This way you know if the reference you are using and the object exist in your scope. using | json gives you accolades {} when your object exists but when it is empty. If your object isn't available in your scope noting is 'printed'.
In your case, change the body tag to:
<body ng-controller="MainCtrl as mainCtrl">
Then, inside the html of this controller add:
DATA: {{mainCtrl | json}}
So now you see your variables! And you also see clicking on the button doesn't change the boolean variable.
With some extra debugging and rewriting, this works:
app.controller('MainCtrl', function($scope) {
var main = this;
this.data = {
document: {
is_provided: true
}
};
$scope.hide = function() {
console.log(main);
main.data.document.is_provided = !main.data.document.is_provided;
}
});
And:
<button ng-click="hide()">Hide</button> (note the brackets)
I'm going through Angular tutorials and seeing code like this:
todoApp.controller("ToDoCtrl", function($scope) {
$scope.todo = model;
$scope.warningLevel = function() {
return $scope.incompleteCount() < 3 ? "label-success" : "label-warning";
}
});
<span ng-class="warningLevel()">Tasks</span>
Like magic, the span changes its class whenever calling warningLevel() would return a new value. But the tutorials don't explain why. I don't understand how Angular "knows" that warningLevel() would return a new value and needs to be called again. What is triggering warningLevel() to be called each time?
As #Linh Pham pointed out this is the so called Two-Way-Databinding. It works this way due to Angular's $digest cycle.
Processes all of the watchers of the current scope and its children. Because a watcher's listener can change the model, the $digest() keeps calling the watchers until no more listeners are firing.
You can imagine it as a static loop which watches for changes and updates the values within the respective $scope. You can also "hook" in to this cycle by binding$watchers manually e.g.:
$scope.$watch('myScopeVar', function() {
// do something
});
I just ran a test i wad expecting to fail, but it works:
I have a collaction of 20 players.
Each players have a number of points.
I want to display into an input the player that have the biggest amount of points.
So what i am going to do is to iterate the collection to find the best one.
So i bind a function that iterate the collection and not the collection itself, and Angular is able to know it has to run the function when the collection is updated, how is that possible ? Does Angular restart all the collection's methods when something is updated into it or is there a smart system which enables it to know which methods are impacted ?
HTML
<input value="{{findTheBest().name}}">
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $timeout) {
$scope.Players = [ { name: 'toto', points: 10 }, { name: 'john', points: 100 } ];
$scope.findTheBest = function() {
TheBest = $scope.Players[0];
for (var Player in $scope.Players) {
if ($scope.Players[Player].points > TheBest.points)
TheBest = $scope.Players[Player];
}
console.log('The Best = '+TheBest.name);
return TheBest;
};
$timeout(function() {
$scope.Players[0].points += 500;
}, 5000);
});
Here is a plunkr
No, Angular isn't doing anything too smart here. When an Angular $digest runs, the watch queue is looped through (in your case findTheBest().name would be in the watch queue).
Then, Angular does dirty checking. So, it basically calls twice during the digest and compares the values. So, the function will be called twice and if the values are different the update is made.
This is a good blog post that describes what the Angular internals are doing.
For each attribute interpolation: {{ expression }} angular registers a $watch.
When a $digest happens , angular iterates over all registered $watchers and evaluates the watchExpressions against their scope.
When your app is initialized, with a first digest loop it evaluates the expression which in your case is also calling a function on that scope. And then your function is registering a $timeout.
$timeout by default triggers another digest loop which in turn triggers another $timeout and so on.
Actually the expression is evaluated every $digest which can happen multiple times on each digest loop. So on each digest loop your function can be called multiple times and set multiple $timeout which is probably not the behavior you expected.
Read more here: http://www.benlesh.com/2013/08/angularjs-watch-digest-and-apply-oh-my.html