I want to use twitter Bootstrap with my application powered by AngularJS. I started with grid layout using Scaffolding http://twitter.github.com/bootstrap/scaffolding.html#gridSystem and run into the following problem:
According to the documentation and examples on Bootstrap, the Grid layout has to follow this structure:
<div class="row">
<div class="span4">...</div>
<div class="span8">...</div>
</div>
<div class="row">
<div class="span4">...</div>
<div class="span8">...</div>
</div>
...
Meaning that tags with 'span' classes (columns) must be child elements of the tags with 'row' classes (rows).
In my app I have a plain array of objects - Projects, which I want to show as 3 Projects in every row. Offcourse I don't know the number of Projects I would have to display. As I understand, this kind of structure requires two nested loops - one for rows and one for columns, which will work perfectly if my model was a two-dimentional array but I don't want to change my model (Projects) to fit the view. What I ended up doing is using filter to change the model to the two dimentional array and then used nested ngRepeat to create the columns: http://jsfiddle.net/oburakevych/h4puc/11/
It seems to work as expected, but I'm getting errors in the debug console:
Error: 10 $digest() iterations reached. Aborting!
From my understanding the nested ng-repeat's digest is triggering digest on the outer ng-repeat? Can anyone suggest a right way of implementing this??
The problem here is that when you use ng-repeat
Angular creates a watch for the list expression, which is a combination of your array of projects and the filter. When Angular runs a digest, it keeps calling that watch until the value of the expression doesn't change any more. And since your filter always creates a new array, the value changes every time it gets called and Angular gets stuck in and endless loop. So your code is basically the same as doing this:
$scope.myList = [];
$scope.$watch('myList', function() {
$scope.myList = [];
});
With a watch on the scope, you can tell Angular to compare by value instead of reference to avoid the endless digest problem, like this:
$scope.myList = [];
$scope.$watch('myList', function() {
$scope.myList = [];
}, true); // Passing true as the last argument triggers comparison by value instead
But that's not possible in your case. So your best bet it to only split the projects array into smaller arrays when needed, something like this:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
<script>
angular.module('myApp', []).controller('Ctrl', function($scope) {
$scope.projects = [
{name: 'My Project 1'},
{name: 'My Project 2'},
{name: 'My Project 3'},
{name: 'My Project 4'},
{name: 'My Project 5'},
{name: 'My Project 6'}
];
var splitIntoRows = function(array, columns) {
if (array.length <= columns) {
return [array];
}
var rowsNum = Math.ceil(array.length / columns);
var rowsArray = new Array(rowsNum);
for (var i = 0; i < rowsNum; i++) {
var columnsArray = new Array(columns);
for (j = 0; j < columns; j++) {
var index = i * columns + j;
if (index < array.length) {
columnsArray[j] = array[index];
} else {
break;
}
}
rowsArray[i] = columnsArray;
}
return rowsArray;
}
$scope.$watch('projects', function() {
$scope.projectRows = splitIntoRows($scope.projects, 3);
});
});
</script>
</head>
<body ng-controller="Ctrl">
<ul class="row" ng-repeat="projectRow in projectRows">
<li class="span4" ng-repeat="project in projectRow">
{{project.name}}
</li>
</ul>
</body>
</html>
If you still want to use a filter for this, you would have to implement caching inside the filter to make sure that you always return the same array reference when the filter is called with the same arguments. But that's a slippery slope, since you need to invalidate that cache to avoid memory leaks.