greenie2600
greenie2600

Reputation: 1699

Why doesn't Angular notice that this variable's value has changed?

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:

(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:

  1. I load the page. Initially, the button is enabled.

  2. 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.

  3. 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

Answers (2)

Kevin B
Kevin B

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

Michael Lorton
Michael Lorton

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

Related Questions