1

I have an ng-repeat looping through an array of objects that belongs to SomeController:

<div ng-controller="SomeController as aCtrl">
  <div ng-repeat="category in aCtrl.categories">
    <p ng-show="aCtrl.checkShouldBeDisplayed(category)">{{category.name}}</p>
  </div>
</div>

The controller is defined as:

app.controller('SomeController', function() {

this.categories = [{
  "name": "one",
}, {
  "name": "two",
}, {
  "name": "three",
}, {
  "name": "four",
}];

this.counter = 0;

this.checkShouldBeDisplayed = function(channel) {
  this.counter = this.counter + 1;
  console.log("executing checkShouldBeDisplayed " + this.counter);
  return true;
};

}); 

I would expect the checkShouldBeDisplayed function to count to 4 as there are four elements in the SomeController.categories array. Instead it counts to 8 - check it here: http://plnkr.co/edit/dyvM49kLLTGof9O92jGb (you'll need to look at the console in your browser to see the log). How can I get around this behaviour? Cheers!

ivalub
  • 33
  • 3

2 Answers2

3

This is expected; The directive ng-repeat will repeat as many times as the framework feels is right; this is called dirty checking and you can read a bit more about it in this post.

For this reason, it is better to think declaratively rather than imperatively. In other words, the method checkShouldBeDisplayed should only do what it says on the tin and not rely on the number of times it is called. If you need to perform an aggregation or manipulation of some sort, that should be done elsewhere in the controller.

Community
  • 1
  • 1
Mendhak
  • 8,194
  • 5
  • 47
  • 64
  • Right, that seems to complicate things. What I am trying to do here is override the normal `ng-show` behaviour, which is bound to the state of the model through some other state that is defined on the controller (imagine having `SomeController.firstUse = true` (instead of the counter), which is reset to `false` once the user interacts with the controller. Can you recommend an Angular way to do this? – ivalub Jun 27 '14 at 20:01
  • If by interacts, you mean click or mouseovers, you can use `ng-click` or `ng-mouseover` and call a method which sets `firstUse` to `false`. If interaction simply involves looking at it, you can instead do it in the `$scope.$on("$destroy"...` where you set it to `false`. If that's not what you meant, please edit your question and add your overall goal. Then we can suggest different approaches. – Mendhak Jun 27 '14 at 21:28
1

According to the docs:

The watchExpression is called on every call to $digest() and should return the value that will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)
[...]
The watch listener may change the model, which may trigger other listeners to fire. This is achieved by rerunning the watchers until no changes are detected.

And all ngShow does is register a watcher:

scope.$watch(attr.ngShow, function ngShowWatchAction(value){
    $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
});

Your function is ngShow's watch-expression and is thus executed twice:
1. In the first $digest it detects the new values.
2. In the second $digest it verifies that nothing has changed and terminates.


In this short demo, you can verify there are two $digest loops.

gkalpak
  • 47,844
  • 8
  • 105
  • 118