13

I'm using angular.js and (for the sake of argument) bootstrap. Now I need to iterate on "things" and display them in ''rows'' :

<div class="row">
  <div class="span4">...</div>
  <div class="span4">...</div>
  <div class="span4">...</div>
</div>
<div class="row">
  etc...

Now, how can I close my .row div on every third thing with angular? I tried ui-if from angular-ui but even that doesn't make it.

If I were to use server-side rendering, I would do something like this (JSP syntax here, but does not matter) :

<div class="row>
  <c:forEach items="${things}" var="thing" varStatus="i">
    <div class="span4">
        ..
    </div>
  <%-- Here is the trick:--%>
  <c:if test="${i.index % 3 == 2}">
          </div><div class="row">
  </c:if>
  </c:forEach>
</div>

Note that I need to actually alter the DOM here, not just css-hiding elements. I tried with the repeat on the .row and .span4 divs, with no avail.

ebottard
  • 1,997
  • 2
  • 12
  • 14
  • write a filter that takes a number n as an argument , that splits an array into an array of arrays of n items then just repeat the main array and repeat sub arrays. – mpm Jan 02 '13 at 14:00

3 Answers3

19

Edit Nov 12, 2013

It seems that not only did angular change a little in 1.2, but that there is an even better method. I've created two filters. I tried to combine them into one but got digest errors. Here are the two filters:

.filter("mySecondFilter", function(){
    return function(input, row, numColumns){
        var returnArray = [];
        for(var x = row * numColumns; x < row * numColumns + numColumns; x++){
            if(x < input.length){
                returnArray.push(input[x]);                    
            }
            else{
                returnArray.push(""); //this is used for the empty cells
            }
        }
        return returnArray;   
    }
})
.filter("myFilter", function(){
    return function(input, numColumns){
        var filtered = [];
        for(var x = 0; x < input.length; x++){
            if(x % numColumns === 0){
                filtered.push(filtered.length);
            }
        }
        return filtered;
    }
});

And now the html will look like this:

<table border="1">
     <tr data-ng-repeat="rows in (objects | myFilter:numColumns)">
          <td data-ng-repeat="column in (objects | mySecondFilter:rows:numColumns)">{{ column.entry }}</td>
     </tr>  
</table>

jsFiddle: http://jsfiddle.net/W39Q2/


Edit Sept 20, 2013

While working with lots of data that needed dynamic columns I've come up with a better method.

HTML:

<table border="1">
    <tr data-ng-repeat="object in (objects | myFilter:numColumns.length)">
        <td data-ng-repeat="column in numColumns">{{ objects[$parent.$index * numColumns.length + $index].entry }}</td>
    </tr>  
</table>

Javascript:

$scope.objects = [ ];
for(var x = 65; x < 91; x++){
    $scope.objects.push({
        entry: String.fromCharCode(x)
    });
}

$scope.numColumns = [];
$scope.numColumns.length = 3;

New Filter:

.filter("myFilter", function(){
    return function(input, columns){
        var filtered = [];
        for(var x = 0; x < input.length; x+= columns){
             filtered.push(input[x]);   
        }
        return filtered;
    }
});

This allows it to be dynamic. To change the columns just change the numColumns.length. In the js fiddle you can see I've wired it up to a dropdown.

jsFiddle: http://jsfiddle.net/j4MPK/


Your html markup would look like this:

<div data-ng-repeat="row in rows">
    <div data-ng-repeat="col in row.col">{{col}}</div>
</div>

And then you could make a variable in your controller like so:

$scope.rows = [
    {col: [ 1,2,3,4 ]},
    {col: [ 5,6,7 ]},
    {col: [ 9,10,11,12 ]}
]; 

This way, you can have any number of columns you want.

jsfiddle http://jsfiddle.net/rtCP3/39/


Edit I've modified the fiddle to now support having a flat array of objects:

jsfiddle: http://jsfiddle.net/rtCP3/41/

The html now looks like this:

<div class="row" data-ng-repeat="row in rows">
    <div class="col" data-ng-repeat="col in cols">
        {{objects[$parent.$index * numColumns + $index].entry}}
    </div>
</div>  

And then in the controller i have:

$scope.objects = [
    {entry: 'a'},
    {entry: 'b'},
    {entry: 'c'},
    {entry: 'd'},
    {entry: 'e'},
    {entry: 'f'},
    {entry: 'g'},
    {entry: 'h'}    
];

$scope.numColumns = 3;
$scope.rows = [];
$scope.rows.length = Math.ceil($scope.objects.length / $scope.numColumns);
$scope.cols = [];
$scope.cols.length = $scope.numColumns;

The $scope.numColumns variable is used to specify how many columns you want in each row.


To handle dynamic array size changes, put a watch on the length of the array (not the whole array, that would be redundent)

$scope.numColumns = 3;  
$scope.rows = [];    
$scope.cols = [];    
$scope.$watch("objects.length", function(){
    $scope.rows.length = Math.ceil($scope.objects.length / $scope.numColumns);
    $scope.cols.length = $scope.numColumns;        
});

jsfiddle: http://jsfiddle.net/rtCP3/45/

Mathew Berg
  • 28,625
  • 11
  • 69
  • 90
15

Why not use something simple like this? http://jsfiddle.net/everdimension/ECCL7/3/

<div ng-controller="MyCtrl as ctr">
    <div class="row" ng-repeat="project in ctr.projects" ng-if="$index % 3 == 0">
         <h4 class="col-sm-4" ng-repeat="project in ctr.projects.slice($index, $index+3)">
            {{project}}
        </h4>
    </div>
</div>
timetowonder
  • 5,121
  • 5
  • 34
  • 48
3

I recommend a directive for a couple of reasons:

  • it can be reused and parameterized in the HTML (i.e., "every 3rd thing" can be a directive attribute)
  • it does not require any controller code/$scope properties, and hence it does not require recalculation of controller $scope properties if the "things" array changes in size

Here is a suggested element directive:

<row-generator row-data=objects col-count=3></row-generator>

In the implementation I used code similar to your server-side example:

myApp.directive('rowGenerator', function() {
    var rowTemplate = '<div class="row">',
        colTemplate = '<div class="span4">';
    return {
        restrict: 'E',
        // use '=' for colCount instead of '@' so that we don't
        // have to use attr.$observe() in the link function
        scope: { rowData: '=', colCount: '='},
        link: function(scope, element) {
            // To save CPU time, we'll just watch the overall
            // length of the rowData array for changes.  You
            // may need to watch more.
            scope.$watch('rowData.length', function(value) {
                var html = rowTemplate;
                for(var i=0; i < scope.rowData.length; i++) {
                    html += colTemplate + scope.rowData[i].key + '</div>';
                    if (i % scope.colCount == scope.colCount - 1) {
                        html += '</div>' + rowTemplate;
                    }
                }
                html += '</div>';
                // don't use replaceWith() as the $watch 
                // above will not work -- use html() instead
                element.html(html);
            })
        }
    }
});

Data:

$scope.things = [
    {key: 'one'},
    {key: 'two'},
    {key: 3},
    {key: 4},
    {key: 'five'},
    {key: 'six'},
    {key: 77},
    {key: 8}    
];

Fiddle

To be efficient, the directive as shown only looks for changes to the length of the rowData (i.e., things) array. If you want to have the directive update the view if the value of one of the array elements changes, you'll need a more expensive $watch:

scope.$watch('rowData', function(value){ ... }, true)

The true at the end does "shallow" dirty checking ("compares the object for equality rather than for reference) -- see docs.

There's one thing I don't like about the directive -- it needs to know that rowData entries have a property with name key:

html += colTemplate + scope.rowData[i].key + '</div>';
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Any suggestion on getting the value from the nested HTML within the `row-generator`, instead of off a `key` property in each object in the list? – Sarah Vessels Nov 20 '13 at 03:45