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!

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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

2 Comments

OK, thanks. Why are there two $digest loops here? Is that because of the dirty checking as @Mendhack is saying?
Yes, it's in my answer (look for 1. and 2.) :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.