Reputation: 1699
I'm having problems with a very simple Angular app. Here's a CodePen; the same code is below.
In this app, you click on a button to eat bananas, and the total number of bananas that you've eaten is shown on the page. After each click, there should be a one-second cooldown before you can eat another banana.
To accomplish this, I've done the following:
$scope.cooling_down
is a boolean.
$scope.can_eat_banana
is a function which returns true
or false
, indicating whether or not the "Eat a Banana" button should be usable. Basically, it returns true if $scope.cooling_down
is false.
The "Eat a Banana" button has ng-disabled="!can_eat_banana()"
.
When the user clicks on the "Eat a Banana" button, I set $scope.cooling_down
to true
, and use window.setTimeout()
to schedule a callback to set it back to false
one second later.
(You may ask: why not get rid of $scope.can_eat_banana()
altogether, and simply use ng-disabled="cooling_down"
on the button? Answer: because, as the game develops, the logic determining whether or not the player can eat a banana will become more complex. And it seems really messy to put that logic in the view.)
So, when I run this test case, this is what happens:
I load the page. Initially, the button is enabled.
I click on the "Eat a Banana" button. As expected, my code sets $scope.cooling_down
to true
, and the button becomes disabled. So far, so good.
One second later, the callback that I passed into window.setTimeout()
fires. (I have a console.log()
as the first line in the callback to confirm that it's running.) This callback sets $scope.cooling_down
back to false
. (You can look at the console to confirm that the value was updated.) But: Angular doesn't notice that the value has changed. The button doesn't become re-enabled, and the {{cooling_down}}
token on the page stays stuck on true
.
Why? Why does Angular notice the first time the value changes (when it gets changed to true
after the user clicks on the button), but not notice the second time (when the callback fires and sets it back to false
)?
<!DOCTYPE html>
<html ng-app="game">
<head>
<meta charset="utf-8">
<title>Incredibly Stupid Banana Game</title>
</head>
<body ng-controller="GameController">
<p>You have eaten {{bananas_eaten}} bananas.</p>
<button type="button" ng-click="eat_banana()" ng-disabled="!can_eat_banana()">Eat a Banana</button>
<p>[current value of cooling_down: {{cooling_down}}]</p>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.min.js"></script>
<script>
( function() {
var app = angular.module( "game", [] );
app.controller( "GameController", [ "$scope", function( $scope ) {
$scope.bananas_eaten = 0;
$scope.cooling_down = false;
$scope.eat_banana = function() {
$scope.bananas_eaten++;
$scope.cooling_down = true;
window.setTimeout( function() {
console.log( "setTimeout callback fired" );
$scope.cooling_down = false;
console.log( "Current value of cooling_down: " + $scope.cooling_down );
}, 1000 );
};
$scope.can_eat_banana = function() {
return !$scope.cooling_down;
};
} ] );
}() );
</script>
</body>
</html>
Upvotes: 1
Views: 380
Reputation: 95022
When you update the model (the property of the scope,) a digest has to occur for that change to affect the UI. By the time the window.setTimeout has completed, the original digest (which was caused by a click) has long since finished and another isn't started, so the UI is never updated.
To solve this, replace window.setTimeout
with the $timeout
service which will start another digest upon the completion of the timeout.
Upvotes: 1
Reputation: 44386
Angular does not run its digest loop again after a window.setTimeout()
(indeed, it is not even notified about it).
Use $timeout()
instead.
Upvotes: 6