3

Edit to add a clear question: I have a flat array of some length and I want to put it into a tr/td type view? This might also be in a bootstrap grid or something like that. Essentially I want to display a flat array in a series of length-n chunks.

There are a number of variations of this question on SO but I haven't really seen a good explanation of either: how to make this work or why it can't. So I've made an extremely simple example that demonstrates the problem. It will render, but if you check the logs you'll see errors (which are too large to link to).

index.html:

<!DOCTYPE html>
<html lang="en-US" ng-app="rowApp">
<head><title>Angular chunks</title>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js" integrity="sha384-c4XWi4+MS7dBmCkPfB02+p/ExOF/ZBOfD2S4KR6mkmpBOg7IM6SUpA1KYZaVr7qE" crossorigin="anonymous"></script>
  <script src="app.js"></script>
</head>
<body>
  <table ng-controller="rowController" border="1" style="width:30%">
    <tr ng-repeat="people_chunk in people | chunk:4">
      <td ng-repeat="person in people_chunk">{{person.name}}</td>
    </td>
  </table>
</body>
</html>

app.js:

var rowApp = angular.module('rowApp', ['filters']);

angular.module('filters', []).
  filter('chunk', function () {
    return function (items, chunk_size) {
      var chunks = [];
      if (angular.isArray(items)) {
        if (isNaN(chunk_size))
          chunk_size = 4;
        for (var i = 0; i < items.length; i += chunk_size) {
          chunks.push(items.slice(i, i + chunk_size));
        }
      } else {
        console.log("items is not an array: " + angular.toJson(items));
      }
      return chunks;
    };
});

rowApp.controller('rowController',
  function ($scope, $http) {
    $http.get("/people.json")
      .then(function(response) { $scope.people = response.data; });
});

people.json:

[{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"},
 {"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}]

You can then serve all this with python -m SimpleHTTPServer 9000 and then go to http://localhost:9000/

Kevin Lyda
  • 992
  • 7
  • 14

3 Answers3

3

You can avoid an infinite digest loop by simply memoizing your chunk function. This solves the issue of ng-repeat never finding the proper references and always thinking you are returning new items causing the infinite $digest.

angular.module('filters', []).
  filter('chunk', function () {
​
    function cacheIt(func) {
      var cache = {};
      return function(arg, chunk_size) {
        // if the function has been called with the argument
        // short circuit and use cached value, otherwise call the
        // cached function with the argument and save it to the cache as well then return
        return cache[arg] ? cache[arg] : cache[arg] = func(arg,chunk_size);
      };
    }
    
    // unchanged from your example apart from we are no longer directly returning this   ​
    function chunk(items, chunk_size) {
      var chunks = [];
      if (angular.isArray(items)) {
        if (isNaN(chunk_size))
          chunk_size = 4;
        for (var i = 0; i < items.length; i += chunk_size) {
          chunks.push(items.slice(i, i + chunk_size));
        }
      } else {
        console.log("items is not an array: " + angular.toJson(items));
      }
      return chunks;
    }
​    // now we return the cached or memoized version of our chunk function
    // if you want to use lodash this is really easy since there is already a chunk and memoize function all above code would be removed
    // this return would simply be: return _.memoize(_.chunk);

    return cacheIt(chunk);
  });
Community
  • 1
  • 1
Rob Soule
  • 66
  • 4
  • Interesting. I saw a similar answer but it assumed you had lodash which I don't just yet. Made an so-answer-2 branch with this in it. One question: could the angular cache work here? – Kevin Lyda Feb 07 '16 at 22:31
  • 1
    its seem to not passing the chunk_size correctly to chunk method. Also caching is based on array being first argument, not a best way to set a property name, I think. – Anuj Pandey Jul 01 '16 at 13:15
  • it works, but changed `cache = {};` to `var cache = {}` because of global scope garbage. – saike Apr 05 '17 at 15:12
2

As the filter is returning new array instances as slices, angular's ngRepeat will detect them changed (as ngRepeat uses $watchCollection internally), and will cause and infinite digest loop. There is even an issue about that, but it is abandoned since 2013: https://github.com/angular/angular.js/issues/2033

This issue still persists if you change the snippet to contain a non-constant expression, like [[x]]:

<div ng-repeat="a in [[x]]">
  <div ng-repeat="b in a">
  </div>
</div>

I am afraid you will have to move the chunking logic into the controller, or to use css to form a 4-width table.

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
  • Ah. And this is why another similar question had a "memoize" call... OK. Thanks. Would rather not have it in the controller, but that is where I eventually put it. Now at least I know why it has to be there. Thanks! – Kevin Lyda Feb 07 '16 at 20:28
  • You should move the chunking logic to a service imho. (but this is a correct answer :) ) – RaidenF Feb 07 '16 at 23:05
0

maybe this could work to someone, you never know

the following code will assign an extra value (store_chunk) to an array of stores. so I can use with ng-repeat to show 3 different columns in my HTML

        var x               = 0;
        var y               = 1;
        var how_many_chunks = 3;
        var limit = $scope.main.stores.length / how_many_chunks ;
        angular.forEach($scope.main.stores, function(e, key) {
            if (x <= limit) {
                $scope.main.stores[key].store_chunk = y;
            }
            else{
                y    += 1;
                limit = y * limit;
                $scope.main.stores[key].store_chunk = y;
            }
            x += 1;
        });

here the HTML

<div class="row">
    <div class="col-xs-12 col-sm-3 col-md-3 col-lg-3">
        <ul class="main_report">
            <li ng-repeat="store in main.stores | filter:{ store_chunk: 3 }">{{store.store_name}}</li>
        </ul>
    </div>
    <div class="col-xs-12 col-sm-3 col-md-3 col-lg-3">
        <ul class="main_report">
            <li ng-repeat="store in main.stores | filter:{ store_chunk: 3 }">{{store.store_name}}</li>
        </ul>
    </div>
    <div class="col-xs-12 col-sm-3 col-md-3 col-lg-3">
        <ul class="main_report">
            <li ng-repeat="store in main.stores | filter:{ store_chunk: 3 }">{{store.store_name}}</li>
        </ul>
    </div>
</div>

this works like a charm! and don't vote down just because you didn't like it!.. thumbs up instead!

Brian Sanchez
  • 832
  • 1
  • 13
  • 11