Iterate Over Chunks of an Array Using Ng-Repeat

iterate over chunks of an array using ng-repeat

After years of angular experience, It's obvious that the best way to do this is to split the array into chunks in the controller. Each time the original array is modified, update the chunked array.

Iterate through array via ngRepeat with breaks

With lodash library you can use chunk function to split an array into chunks and then build a list that suits your needs.

So basically just iterate as you would do for a normal list of arrays (which represent your rows containing goods) and implement a function that build this list according to the divider. That way you can invoke the function when you need to rebuild your list (on keyup of the input in the example bellow) and let AngularJS do the rest.

(function(angular) {
'use strict'; angular.module('ngRepeat', []) .controller('repeatController', function($scope) { // the goods $scope.goods = [ { name: "name-1", price: 1.01, description: 'desc-1' }, { name: "name-2", price: 2.02, description: 'desc-2' }, { name: "name-3", price: 3.03, description: 'desc-3' }, { name: "name-4", price: 4.04, description: 'desc-4' }, { name: "name-5", price: 5.05, description: 'desc-5' }, { name: "name-6", price: 6.06, description: 'desc-6' }, { name: "name-7", price: 7.07, description: 'desc-7' }, { name: "name-8", price: 8.08, description: 'desc-8' }, { name: "name-9", price: 9.09, description: 'desc-9' }, { name: "name-10", price: 10.10, description: 'desc-10' }, { name: "name-11", price: 11.11, description: 'desc-11' }, { name: "name-12", price: 12.12, description: 'desc-12' } ];
// divider determines how many goods per row (defaulted to 4) $scope.divider = 4; // function that build the rows of goods $scope.dividerChanged = function() { $scope.rows = _.chunk($scope.goods, $scope.divider); }; // initialize rows on first load $scope.dividerChanged(); });})(window.angular);
.divider {  margin-bottom: 10px;}  .goods-row {  border: 1px solid blue;  padding: 10px;  text-align: center;}
.good { border: 1px solid red; display: inline-block; padding: 10px; margin: 10px; width: 50px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.min.js"></script><script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<body ng-app="ngRepeat"> <div ng-controller="repeatController"> divider: <input type="text" ng-model="divider" ng-keyup="dividerChanged()" class="divider"> <div class="goods"> <div class="goods-row" ng-repeat="row in rows"> <div class="good" ng-repeat="good in row"> <button id="add_to_cart">+</button> <div class="descr"> <div class="descr-top"> <h5 class="g-name">{{ good.name }}</h5> <span class="g-price">{{ good.price | currency }}</span> </div> <div class="descr-mid">{{ good.description }}</div> </div> </div> </div> </div> </div></body>

AngularJS: Can I use a filter to chunk an array in ng-repeat?

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);
});

How can I repeat certain chunks of object with ng-repeat?

I fixed it by changing my structure to the following format:

{
"chunk1": {
"ind1":
{
"title": 'AC',
"active": true
}, "ind2":{
"title": 'Aux PS',
"active": true
}, "ind3":{
"title": 'Main PS',
"active": false
},
"ind4": {
"title": 'Shutdown',
"active": true
}
},

"chunk2": {
"ind1": {
"title": 'Tx',
"active": false
}, "ind2": {
"title": 'Rx',
"active": true
}
}
}

ng-repeat in $scope.array of $scope.array of obeject

use nested repeats:

<tr ng-repeat="row in rows">
<td ng-repeat="col in row">{{col}}</td>
</tr>

where rows is an array of arrays: [Array[2], ...], and each nested array contains the column objects

how to split the ng-repeat data with three columns using bootstrap

The most reliable and technically correct approach is to transform the data in the controller. Here's a simple chunk function and usage.

function chunk(arr, size) {
var newArr = [];
for (var i=0; i<arr.length; i+=size) {
newArr.push(arr.slice(i, i+size));
}
return newArr;
}

$scope.chunkedData = chunk(myData, 3);

Then your view would look like this:

<div class="row" ng-repeat="rows in chunkedData">
<div class="span4" ng-repeat="item in rows">{{item}}</div>
</div>

If you have any inputs within the ng-repeat, you will probably want to unchunk/rejoin the arrays as the data is modified or on submission. Here's how this would look in a $watch, so that the data is always available in the original, merged format:

$scope.$watch('chunkedData', function(val) {
$scope.data = [].concat.apply([], val);
}, true); // deep watch

Many people prefer to accomplish this in the view with a filter. This is possible, but should only be used for display purposes! If you add inputs within this filtered view, it will cause problems that can be solved, but are not pretty or reliable.

The problem with this filter is that it returns new nested arrays each time. Angular is watching the return value from the filter. The first time the filter runs, Angular knows the value, then runs it again to ensure it is done changing. If both values are the same, the cycle is ended. If not, the filter will fire again and again until they are the same, or Angular realizes an infinite digest loop is occurring and shuts down. Because new nested arrays/objects were not previously tracked by Angular, it always sees the return value as different from the previous. To fix these "unstable" filters, you must wrap the filter in a memoize function. lodash has a memoize function and the latest version of lodash also includes a chunk function, so we can create this filter very simply using npm modules and compiling the script with browserify or webpack.

Remember: display only! Filter in the controller if you're using inputs!

Install lodash:

npm install lodash-node

Create the filter:

var chunk = require('lodash-node/modern/array/chunk');
var memoize = require('lodash-node/modern/function/memoize');

angular.module('myModule', [])
.filter('chunk', function() {
return memoize(chunk);
});

And here's a sample with this filter:

<div ng-repeat="row in ['a','b','c','d','e','f'] | chunk:3">
<div class="column" ng-repeat="item in row">
{{($parent.$index*row.length)+$index+1}}. {{item}}
</div>
</div>

Order items vertically

1  4
2 5
3 6

Regarding vertical columns (list top to bottom) rather than horizontal (left to right), the exact implementation depends on the desired semantics. Lists that divide up unevenly can be distributed different ways. Here's one way:

<div ng-repeat="row in columns">
<div class="column" ng-repeat="item in row">
{{item}}
</div>
</div>
var data = ['a','b','c','d','e','f','g'];
$scope.columns = columnize(data, 3);
function columnize(input, cols) {
var arr = [];
for(i = 0; i < input.length; i++) {
var colIdx = i % cols;
arr[colIdx] = arr[colIdx] || [];
arr[colIdx].push(input[i]);
}
return arr;
}

However, the most direct and just plainly simple way to get columns is to use CSS columns:

.columns {
columns: 3;
}
<div class="columns">
<div ng-repeat="item in ['a','b','c','d','e','f','g']">
{{item}}
</div>
</div>

rxjs angular2 iterate over array with infinite loop

The problem with your code is that you placed take after repeat, thus take will unsubscribe both the source and repeat.

Another option would be to just take the % nth element of the array, since interval emits numbers, we can use that. I noticed you added startWith to interval. Another way to get an interval that starts without delay is to use timer(0, interval). Thus our final code might look like:

Observable.timer(0, 1000)
.map(e => array[e % array.length])
.subscribe(item => { ... })


Related Topics



Leave a reply



Submit