12

Following angular.js conditional markup in ng-repeat, I tried to author a custom filter that does grouping. I hit problems regarding object identity and the model being watched for changes, but thought I finally nailed it, as no errors popped in the console anymore.

Turns out I was wrong, because now when I try to combine it with other filters (for pagination) like so

<div ng-repeat="r in blueprints | orderBy:sortPty | startFrom:currentPage*pageSize | limitTo:pageSize | group:3">
      <div ng-repeat="b in r">

I get the dreaded "10 $digest() iterations reached. Aborting!" error message again.

Here is my group filter:

filter('group', function() {
  return function(input, size) {
    if (input.grouped === true) {
      return input;
    }
  var result=[];
  var temp = [];
  for (var i = 0 ; i < input.length ; i++) {
      temp.push(input[i]);
      if (i % size === 2) {
          result.push(temp);
          temp = [];
      }
  }
  if (temp.length > 0) {
      result.push(temp);
  }
  angular.copy(result, input);
  input.grouped = true;
  return input;
}; 
}).

Note both the use of angular.copy and the .grouped marker on input, but to no avail :( I am aware of e.g. "10 $digest() iterations reached. Aborting!" due to filter using angularjs but obviously I did not get it.

Moreover, I guess the grouping logic is a bit naive, but that's another story. Any help would be greatly appreciated, as this is driving me crazy.

Community
  • 1
  • 1
ebottard
  • 1,997
  • 2
  • 12
  • 14
  • Never mind I just found this additional thread http://stackoverflow.com/questions/14800862/how-can-i-group-data-with-an-angular-filter – Eddie A. Jan 21 '15 at 21:08

3 Answers3

14

It looks like the real problem here is you're altering your input, rather than creating a new variable and outputing that from your filter. This will trigger watches on anything that is watching the variable you've input.

There's really no reason to add a "grouped == true" check in there, because you should have total control over your own filters. But if that's a must for your application, then you'd want to add "grouped == true" to the result of your filter, not the input.

The way filters work is they alter the input and return something different, then the next filter deals with the previous filters result... so your "filtered" check would be mostly irrelavant item in items | filter1 | filter2 | filter3 where filter1 filters items, filter2 filters the result of filter1, and filter3 filters the result of filter 2... if that makes sense.

Here is something I just whipped up. I'm not sure (yet) if it works, but it gives you the basic idea. You'd take an array on one side, and you spit out an array of arrays on the other.

app.filter('group', function(){
   return function(items, groupSize) {
      var groups = [],
         inner;
      for(var i = 0; i < items.length; i++) {
         if(i % groupSize === 0) {
            inner = [];
            groups.push(inner);
         }
         inner.push(items[i]);
      }
      return groups;
   };
});

HTML

<ul ng-repeat="grouping in items | group:3">
    <li ng-repeat="item in grouping">{{item}}</li>
</ul>

EDIT

Perhaps it's nicer to see all of those filters in your code, but it looks like it's causing issues because it constantly needs to be re-evaluated on $digest. So I propose you do something like this:

app.controller('MyCtrl', function($scope, $filter) {
   $scope.blueprints = [ /* your data */ ];
   $scope.currentPage = 0;
   $scope.pageSize = 30;
   $scope.groupSize = 3;
   $scope.sortPty = 'stuff';

   //load our filters
   var orderBy = $filter('orderBy'),
       startFrom = $filter('startFrom'),
       limitTo = $filter('limitTo'),
       group = $filter('group'); //from the filter above

   //a method to apply the filters.
   function updateBlueprintDisplay(blueprints) {
        var result = orderBy(blueprints, $scope.sortPty);
        result = startForm(result, $scope.currentPage * $scope.pageSize);
        result = limitTo(result, $scope.pageSize);
        result = group(result, 3);
        $scope.blueprintDisplay = result;
   }

   //apply them to the initial value.
   updateBlueprintDisplay();

   //watch for changes.
   $scope.$watch('blueprints', updateBlueprintDisplay);
});

then in your markup:

<ul ng-repeat="grouping in blueprintDisplay">
   <li ng-repeat="item in grouping">{{item}}</li>
</ul>

... I'm sure there are typos in there, but that's the basic idea.


EDIT AGAIN: I know you've already accepted this answer, but there is one more way to do this I learned recently that you might like better:

<div ng-repeat="item in groupedItems = (items | group:3 | filter1 | filter2)">
    <div ng-repeat="subitem in items.subitems">
    {{subitem}}
    </div>
</div>

This will create a new property on your $scope called $scope.groupedItems on the fly, which should effectively cache your filtered and grouped results.

Give it a whirl and let me know if it works out for you. If not, I guess the other answer might be better.

Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 1
    I read what you say, but indeed I went thru all those tricks because it appeared that angular detects modification by reference equality. What you propose is basically what I had initially, but it proved problematic. Just for the record, how many times should the group() function be invoked by angular do you think? I was amazed to discover that it was not 1. My later understanding was that it would stop calling it when it "stabilized" _i.e._ when angular detects that it gets the "same" result for the same input. The question lies in how does it interpret "same" I guess. Hope that makes sense – ebottard Jan 22 '13 at 16:40
  • Your solution has two problems btw, it does not account for the last group and resets the inner array before pushing it. Even after correction, this does not work. I created a fiddle for this : http://jsfiddle.net/cSEAb/ Open the console and see how many times the function gets called (followed by an exception) – ebottard Jan 22 '13 at 17:05
  • You've actually broken it. Notice you're now pushing a null value onto the groups array. `i` starts off as 0, so the first time that if statement is hit, it's evaluating as true. The only bug I see is I was saying `ng-repeat="grouping in item | group:3"` which is a typo and should have been item**s**. I'll correct that. – Ben Lesh Jan 22 '13 at 17:19
  • regardless, I'm still seeing the $digest error, which is puzzling: http://plnkr.co/edit/tHm8uYfjn8EJk3cG31DP – Ben Lesh Jan 22 '13 at 17:21
  • you're right, sorry. Did not see that you had taken that approach in pushing the inner array. Anyway, as you said, the core problem still persists – ebottard Jan 22 '13 at 17:36
  • Okay, so at this point it looks like ng-repeat doesn't like operating on the result of a filter because when it does, that result doesn't always exist. If you look at the last 5 watches returned when the $digest error is thrown, it shows ngRepeat watches with oldVal and newVal equal to undefined. That means you'll have to group your items into something that is scoped, and/or do the grouping at the controller level, and/or create a new directive to handle repeating the grouping. – Ben Lesh Jan 22 '13 at 17:51
  • Hmm, can you elaborate on your proposed solutions? I kind of liked the ability to chain the filters, as explained in my original question (I do pagination, sorting, etc) – ebottard Jan 22 '13 at 21:43
  • Reading through the code of $digest() I still don't understand why this behaves the way it does. I guess my objects (and even more the simple strings we used in the fiddle) are equal according to equals(), so I'm puzzled. – ebottard Jan 22 '13 at 22:14
  • Well, the work around I was thinking of was to have a method that created the object structure as you want it displayed. This would be a lot more efficient, too, as it wouldn't have to be processed on every $digest. You could even use the filters, I'll put some psuedo-code in my answer, maybe that will help. – Ben Lesh Jan 23 '13 at 13:54
  • OK, I see how this would go. I'll give it a try asap. Thanks for the time spent in any case. – ebottard Jan 23 '13 at 15:48
  • Seems that it does the trick (modulo typos which I'll edit in your posts), with one caveat: I need to watch on everything that affects rendering, _e.g._ `currentPage` and `sortPty`. I'm still clueless about the difference between your solution and the builtin filter with pipes solutions, since those should endup doing the same. Thanks anyway, very appreciated. – ebottard Feb 01 '13 at 10:36
  • @blesh - Thanks for the tip on caching filtered array in view. Does it even get mentioned in the official documentation? May I ask for the source that you learned it from? – tamakisquare Sep 26 '13 at 17:16
  • This filter seems to do what I need (group items by a defined value). However i'm a little lost when it comes to everything regarding the $digest issue noted above. Is there a solution I can use that wouldn't require me to do something at the controller level? Also, is there any way to identify the FIRST grouping at the template level using this filter? – flashpunk Feb 07 '14 at 21:45
3

Regardless, I'm still seeing the $digest error, which is puzzling: plnkr.co/edit/tHm8uYfjn8EJk3cG31DP – blesh Jan 22 at 17:21

Here is the plunker forked with the fix to the $digest error, using underscore's memoize function: http://underscorejs.org/#memoize.

The issue was that Angular tries to process the filtered collection as a different collection during each iteration. To make sure the return of the filter always returns the same objects, use memoize.

http://en.wikipedia.org/wiki/Memoization

Another example of grouping with underscore: Angular filter works but causes "10 $digest iterations reached"

Community
  • 1
  • 1
user2779653
  • 918
  • 1
  • 9
  • 26
1

You can use groupBy filter of angular.filter module, and do something like this:
usage: (key, value) in collection | groupBy: 'property'or 'propperty.nested'
JS:

$scope.players = [
  {name: 'Gene', team: 'alpha'},
  {name: 'George', team: 'beta'},
  {name: 'Steve', team: 'gamma'},
  {name: 'Paula', team: 'beta'},
  {name: 'Scruath', team: 'gamma'}
];

HTML:

<ul ng-repeat="(key, value) in players | groupBy: 'team'" >
  Group name: {{ key }}
  <li ng-repeat="player in value">
    player: {{ player.name }} 
  </li>
</ul>
<!-- result:
  Group name: alpha
    * player: Gene
  Group name: beta
    * player: George
    * player: Paula
  Group name: gamma
    * player: Steve
    * player: Scruath
a8m
  • 9,334
  • 4
  • 37
  • 40