2

Below is a very simple TODO app build with angularjs.

It works fine so far, but my problem is that angularjs keeps on calling the 'remaining' function on evry keystroke in the input field!! is there anything wrong with the below code?

<!doctype html>
<html ng-app>

<head>
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    <link href="style.css" rel="stylesheet">
</head>

<body>
    <div>
        <h2>TODO:</h2>
        <div ng-controller="TodoCtrl">
            <span>{{remaining()}} of {{todos.length}} remaining</span> [ <a href="" ng-click="archive()">archive</a> ]
            <ul class="list-unstyled">
                <li ng-repeat="todo in todos">
                    <input type="checkbox" ng-model="todo.done" >
                    <span>{{todo.text}}</span>
                </li>
            </ul>
            <form ng-submit="addTodo()">
                <input type="text" ng-model="todo" placeholder="Enter your task here">
            <form>
        </div>
    </div>

    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js"></script>
    <!--script src="app.js"></script-->
    <script>
    function TodoCtrl($scope) {
        $scope.todos = [
            {text: 'learn angular', done:true},
            {text: 'build an angular app', done:false}
        ];

        $scope.remaining = function() {
            console.log('calling remaining()');
            var count = 0;
            angular.forEach($scope.todos, function(todo) {
                count += todo.done? 0:1;
            });

            return count;
        };

        $scope.addTodo =  function() {
            if($scope.todo)  {
                $scope.todos.push({text: $scope.todo, done:false});
            }
            $scope.todo = '';
        };
    }
    </script>
</body>
</html>
user327843
  • 482
  • 6
  • 17
  • 2
    Nothing wrong here, it calls `remaining()` on every digest cycle. and $digest cycle fired on any input change – Maxim Shoustin Jan 06 '14 at 13:44
  • every time that you change the model that is binding to the input, angular will call the $disgest cicle to update the models and bindings. You have binding the todos variable and also the remaining function. It is normal this function to be called in this case. – Deividi Cavarzan Jan 06 '14 at 13:48
  • possible duplicate of [Expression evaluated 2 times](http://stackoverflow.com/questions/15078231/expression-evaluated-2-times) – Stewie Jan 06 '14 at 14:06

2 Answers2

4

This is standard angular behavior. On any change to the model or any other binding or angular event it will execute all watches that are setup on the scope. This is called a digest cycle and is usually triggered by a $scope.$apply(). This is why it is extremely important to not do any heavy calculation on functions that are called from a binding expression in the view.

Performance problems happen usually when having functions that do some calculation or filtering on a long list. If this is an issue, the solution is to setup a watch on the collection and update the calculated property as a separat variable in the scope only when the collection changes. In your example, this would prevent recalculating the remaining items in cases where unrelated inputs change.

Daniel Tabuenca
  • 13,147
  • 3
  • 35
  • 38
4

To supplement @dtabuenc's answer, here is an example of how prevent that function firing on every minor change:

Replace:

<span>{{remaining()}} of {{todos.length}} remaining</span> [ <a href="" ng-click="archive()">archive</a> ]

With:

<span>{{remaining}} of {{todos.length}} remaining</span> [ <a href="" ng-click="archive()">archive</a> ]

And replace the definition of $scope.remaining = function()... with:

$scope.$watch('todos', function(todos) {
  var count = 0;
  angular.forEach(todos, function(todo) {
    count += todo.done ? 0 : 1;
  }
  $scope.remaining = count;
}

This will prevent angular from evaluating the remaining() angular expression in your view for every scope change.