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 and 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>
I fix without .row
<div class="col col-33 left" ng-repeat="photo in photos">
Content here...
</div>
and css
.left { float: left; }