15

I'm trying to build a pagination directive with angularjs 1.2.15:

This is my view:

<input type="text" ng-model="filter.user">
<input type="number" ng-model="filter.limit" ng-init="filter.limit=5">
<ul>
  <li ng-repeat="user in (filteredUsers = (users | orderBy:order:reverse | filter:filter.user ) | limitTo: filter.limit)" ng-click="showDetails(user)">
    {{user.id}} / {{user.firstname}} {{user.lastname}}
  </li>
</ul>
<div pagination data='filteredUsers' limit='filter.limit'></div>

and here is my pagination directive:

app.directive('pagination', function(){
  return {
    restrict: 'A',
    templateUrl: 'partials/pagination.html',
    scope: {
      data: '=data',
      limit: '=limit'
    }
  }
})

Everything works perfectly fine without the pagination directive. However with my new directive as soon as I load the page I get a $rootScope:infdig error which I don't understand since the directive is not doing anything to manipulate data that could end up in an infinite loop.
What is the problem here and how can I solve it? Thanks!

Update:
Here are the controller and the resource.
Controller:

usersModule.controller('usersController',
  function ($scope, Users) {
    function init(){
      $scope.users = Users.get();
    }
    init();
})

Resource (gets users as an array from a REST API):

app.factory('Users', function($resource) {
  return $resource('http://myrestapi.tld/users', null,{
       'get': { method:'GET', isArray: true, cache: true }
   });
});

Update 2

Here is a demo: http://plnkr.co/edit/9GCE3Kzf21a7l10GFPmy?p=preview
Just type in a letter (e.g. "f") into the left input.

6
  • Please specify code for controller so that people can reach out to problem. I think above directive is perfect. There can be issue with your controller. Commented Apr 8, 2014 at 4:52
  • @JenishRabadiya I updated the code above. However, I don't think it has anything to do with my controller... Commented Apr 8, 2014 at 8:22
  • By the way, the error doesn't occur when passing users instead of filteredUsers into the directive... Commented Apr 8, 2014 at 8:37
  • @Horen: Such cases lend themselves nicely to a fiddle (saving everyone loads of time) :) Commented Apr 8, 2014 at 9:45
  • @Horen: It is obviously something in your pagination template, so sharing that as well might help... Commented Apr 8, 2014 at 11:02

3 Answers 3

31
+50

The problem is not within the directive, it's within the $watch the directive creates. When you send filteredUsers to the directive, the directive creates the following line:

$scope.$watch("filteredUsers", function() {
    // Directive Code...
});

Notice in the following example how we reproduce it without a directive: http://plnkr.co/edit/uRj19PyXkvnLNyh5iY0j

The reason it happens is because you are changing filteredUsers every time a digest runs (since you put the assignment in the ng-repeat statement).

To fix this you might consider watching and filtering the array in the controller with the extra parameter 'true' for the $watch:

$scope.$watch("users | orderBy:order:reverse | filter:filter.user", function(newVal) {
    $scope.filteredUsers = newVal;
}, true);

You can check the solution here: http://plnkr.co/edit/stxqBtzLsGEXmsrv3Gp6

the $watch without the extra parameter (true) will do a simple comparison to the object, and since you create a new array in every digest loop, the object will always be different. When you're passing the true parameter to the $watch function, it means it will actually do deep comparison to the object that returns before running the $watch again, so even if you have different instances of arrays that has the same data it will consider them equal.

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

5 Comments

Yes, that seems to solve the problem. I don't quite understand why the $watch() function in your first example changes anything, though. I understand it is triggered when the filters are applied or changed. However the $watch() function does not even manipulate or change the filteredUsers object in any way. So how is an infinite loop generated? Even if the $watch() function creates a new object with every digest this object shouldn't change, right? It would be great if you could clarify - thanks so far!
Since in your ng-repeat statment you create the variable filteredUsers - Your statement is: filteredUsers = (users | orderBy:order:reverse | filter:filter.user ), it means that in every digest cycle, filteredUsers gets a new object reference. When you use the $watch method, it checks if the reference has changed and if it has, it considers the watch "dirty", runs the callback method registered to it and then runs the digest loop again - in the next digest loop filteredUsers will change again, so the $watch callback will be called again - and here is your infinite loop...
Ok, thanks. Why however isn't it enough to declare the $scope.filteredUsers object in the init() function once? I mean, then angular should know that it's always the same object, right?
Well, if you use $scope.filteredUsers = [1,2,3] for example it would define a new array and whenever you change a value within it, it will not trigger a simple watch (change like $scope.filteredUsers[0] = 5) but if you assign a new value to the whole variable it changes, for example $scope.filteredUsers = [5,2,3] will trigger a watch. Try the following row in the console and you'll see it returns false: [5,2,3] === [5,2,3]. But if you try a1 = [1,2,3]; a2 = a1; a1[0] = 5 you'll see that a1 === a2 returns true. In the ng-repeat, you always create a new instance.
I had similar issue. I added ng-cloak to view part of causing element. It resolved the issue.
0

A quick fix is to add a "manual" $watchCollection in the directive, instead of a 2-way binding.

app.directive('pagination', function($parse){
  return {
    restrict: 'A',
    template: '',
    scope: {
      limit: '=limit'
    },
    link: function(scope, elem, attrs) {
      var dataExpr = $parse(attrs.data);
      var deregister = scope.$parent.$watchCollection(dataExpr, function(val) {
        scope.data = val;
      });
      scope.$on('$destroy', deregister);
    }
  }
})

$watchCollection monitors the contents of the array, not the reference to it.

See it running here.

Generally speaking, i don't like expressions like that:

filteredUsers = (users | orderBy:order:reverse | filter:filter.user )

inside views. Views should only render $scope properties, not create new ones.

2 Comments

I don't understand how this works with the isolated scope. How will be the isolated scope be able to evaluate the attrs.data expression correctly ?
@NicolaeSurdu, you're right. You have to register the watch on the parent scope and deregister when the child scope dies. I edited the code.
-1

This error may remove to clear the browser history from setting. I got same issue and apply many solution to resolve this, But cannot resolve. But when I remove browser history and cache this issue is resolve. May this help for you.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.