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 % n
th 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
Why Doesn't CSS-Calc() Work When Using 0 Inside the Equation
Why Can't I Beat an Id with Multiple Classes
Prefer Shrinking Over Growing in a Flex Container with Flex-Flow: Row Wrap
Enforce Print Page Breaks with CSS
:Hover Not Working on Svg When Svg Is in External File
How to Center One of the Flex/Grid Children(More Than Three) and with Different Widths
Gmail Responsive Email - Media Queries - Style Tag
Transform-Origin Not Working in Firefox Even Properties in Percentage Value
Box-Shadow Not Shown on Safari Mobile on iOS 7 (In Landscape)
CSS Variables - Swapping Values
Triangle with One Rounded Corner
Css: Adding a Border Changes the Background-Color (!)
How to Add Tableview Footer in Javafx Tableview
Css3 Slanted Div with Background Image, with Image Behind the Whole Thing