46

I'm trying to create a grid using bootstrap 3 and angularjs.

The grid I'm trying to create is this, repeated using ng-repeat.

<div class="row">
 <div class="col-md-4">item</div>
 <div class="col-md-4">item</div>
 <div class="col-md-4">item</div>
</div>

I've tried using ng-if with ($index % 3 == 0) to add the rows, but this doesn't seem to be working right. Any suggestions would be great!

Thank you!

EDIT: Here's the code I ended up going with that worked:

<div ng-repeat="item in items">
  <div ng-class="row|($index % 3 == 0)">
    <ng-include class="col-sm-4" src="'views/items/item'"></ng-include> 
  </div>
</div>
dzm
  • 22,844
  • 47
  • 146
  • 226
  • 3
    Will be better if you put a http://plnkr.co with current code – dimirc Nov 22 '13 at 05:22
  • 1
    The solution provided doesn't seem to work quite well. I've added an answer below with two techniques to do grids with flat lists. [plunker](http://plnkr.co/edit/7DHxoHNhmUepbyWQEdr5?p=preview) here. – CodeExpress Sep 14 '14 at 21:19

11 Answers11

73

The accepted answer is the obvious solution however presentation logic should remain in view and not in controllers or models. Also I wasn't able to get the OP's solution to work.

Here are two ways to do create grid system when you have a flat list(array) of items. Say our item list is a alphabet:

Plunker here

$scope.alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
                   'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

Method 1:

This is a pure angular solution.

<div class="row" ng-repeat="letter in alphabet track by $index" ng-if="$index % 4 == 0">
  <div class="col-xs-3 letter-box" 
       ng-repeat="i in [$index, $index + 1, $index + 2, $index + 3]" 
       ng-if="alphabet[i] != null">
    <div>Letter {{i + 1}} is: <b> {{alphabet[i]}}</b></div>
  </div>
</div>

The outer loop execute after every 4 iterations and creates a row. For each run of the outer loop the inner loop iterates 4 times and creates columns. Since the inner loop runs 4 times regardless of whether we have elements in array or not, the ng-if makes sure that no extraneous cols are created if the array ends before inner loop completes.

Method 2:

This is much simpler solution but requires angular-filter library.

<div class="row" ng-repeat="letters in alphabet | chunkBy:4">
  <div class="col-xs-3 letter-box" ng-repeat="letter in letters" >
    <div>Letter {{$index + 1}} is: <b> {{letter}}</b></div>
  </div>
</div>

The outer loop creates groups of 4 letters, corresponding to our 'row'

[['A', 'B', 'C', 'D'], ['E', 'F', 'G', 'H'], ... ]

The inner loop iterates over the group and creates columns.

Note: Method 2 might require evaluation of filter for each iteration of outer loop, hence method 2 may not scale very well for huge data sets.

Shalom Craimer
  • 20,659
  • 8
  • 70
  • 106
CodeExpress
  • 2,202
  • 1
  • 20
  • 16
  • 3
    This is much better than my answer (I really agree with keeping presentation logic out of the controller, it can get really messy if you don't). Also, the filter in method 2 should be really easy to write on your own even if you don't user angular-filter. – Erik Honn Nov 21 '14 at 09:41
  • Are you sure method 2 still works? It didn't for me, and the source seems to indicate that it only takes object keys and always returns an object (not an array) https://github.com/a8m/angular-filter/blob/master/src/_filter/collection/group-by.js – srlm Feb 12 '15 at 07:00
  • @srlm, It works in the plunker I've linked: http://plnkr.co/edit/7DHxoHNhmUepbyWQEdr5?p=preview Though I see that I've used v0.4.6 of angular-filter and its current version is v0.5.3. I'll retest with the newer version and update the answer/comments appropriately – CodeExpress Feb 17 '15 at 06:02
  • @Shivam: why can't I change it to `groupBy: 3` in your plnkr? Doing so does not change anything. – Clawish Feb 24 '15 at 21:30
  • 2
    @Clawish, If you want to have 3 columns then you'll also need to change the column class to col-xs-4. There are 12 columns in bootstrap, so 3 columns means each column takes 4 blocks, hence col-xs-4. If you keep it unchanged to col-xs-3, the gouping still happens but the columns just 'wrap up' to make no visual difference. Check http://plnkr.co/edit/L207RC5Dmxk61fusC2rr?p=preview – CodeExpress Feb 25 '15 at 05:25
  • @Shivam does it also work with filters like orderBy ? I created an own filter, and the filter loses his purpose when using Method 1. – Betty St May 11 '15 at 16:11
  • 1
    ok I found a solution, you need to use 'as': ``item in items | filter:x as results`` @see https://docs.angularjs.org/api/ng/directive/ngRepeat – Betty St May 12 '15 at 13:52
  • Method 2 doesn't work.. If you look at my answer on here I created a filter that works wonderfully. – user1943442 Jun 02 '15 at 15:17
  • 1
    @user1943442, can you elaborate on how the method 2 doesn't work ? The code in plunker is working – CodeExpress Jun 02 '15 at 19:02
  • @Shivam It just appears as if it's working because the column width is set to a 3 (a quarter)... if you set your groupby to 3 or 4 without changing the bootstrap class you will notice that it's not actually working. Also if you view the source you will see they are all in 1 row. If you look at the plunker I created and change the splitrow filter to whichever number you want it will work accordingly. – user1943442 Jun 04 '15 at 02:28
  • CodeExpress, I like Method2 implementation. works fine with array, But what if I using Object and scan the object like this : "ng-repeat="(key, value) in data" ? how should I write the second ngrepeat ? – john Smith Sep 15 '15 at 16:13
  • 4
    Just a heads up, groupBy is now chunkBy: n in angular-filter – Dave Chenell Jan 07 '16 at 04:46
23

This is an old answer!

I was still a bit new on Angular when I wrote this. There is a much better answer below from Shivam that I suggest you use instead. It keeps presentation logic out of your controller, which is a very good thing.

Original answer

You can always split the list you are repeating over into a list of lists (with three items each) in your controller. So you list is:

$scope.split_items = [['item1', 'item2', 'item3'], ['item4', 'item5', 'item6']];

And then repeat it as:

<div ng-repeat="items in split_items" class="row">
    <div ng-repeat="item in items" class="col-md-4">
        item
    </div>
</div>

Not sure if there is a better way. I have also tried playing around with ng-if and ng-switch but I could never get it to work.

Erik Honn
  • 7,576
  • 5
  • 33
  • 42
  • Erik, thanks. I actually found as solution that I added to my original post. I'll go ahead and accept you however for a working alternative – dzm Nov 22 '13 at 20:09
  • 1
    The solution you found does not actually do what you wanted it to do. It just wraps every third element in a row, it does not wrap all three elements. Also, that way of writing an ng-class does not even seem to work, but perhaps we are using different versions of angular. – Erik Honn Nov 22 '13 at 21:34
  • I think "item" within the inner div needs to be in double-curly-braces, no? {{item}} – adampasz Mar 16 '15 at 18:28
  • 1
    Yes, "item" was just my way of saying "put your stuff here". Should probably not have used the same keyword as I used in the actual code :) Anyway, you should look at the answer from Shivam instead, it is much better since it works with flat lists and keeps presentation logic out of the controller! – Erik Honn Mar 18 '15 at 14:37
9

You can simply chunk your array into subarrays of N inside of your controller. Sample code:

var array = ['A','B','C','D','E','F','G','H'];

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

$scope.array = chunk(array, 2);

Now in *.html file You simply ng-repeat through the array

<div class="row" ng-repeat="chunk in array">
    <div class="col-md-6" ng-repeat="item in chunk">
         {{item}}
    </div>
</div>

That workout for me :) Good luck!

ndeverge
  • 21,378
  • 4
  • 56
  • 85
Mark Okhman
  • 564
  • 6
  • 13
3

One might say that the below solution does not follow the grid rules of having row divs, but another solution would be to drop the row class ( or use it outside of the ng-repeat) and use the clearfix class instead:

<div class="col-md-12">
  <div ng-repeat="item in items">
    <div ng-class="{'clearfix': $index%3 === 0}"></div>
    <div class="col-md-4">{{item}}</div>
  </div>
</div>

As far as I can see, this looks almost the same as with row class, but please comment on possible flaws (except the one I mentioned above).

3

Using ng-repeat-start and ng-repeat-end

<div class="row">
    <div ng-repeat-start="item in items track by $index" class="col-sm-4">
      {{item}}
    </div>
    <div ng-repeat-end ng-if="($index+1) % 3 == 0" class="clearfix"></div>
</div>

Easy to adapt for different media query using .visible-* classes

<div class="row">
    <div ng-repeat-start="item in items track by $index" class="col-lg-2 col-md-4 col-sm-6">
      {{item}}
    </div>
    <div ng-repeat-end>
        <div class="clearfix visible-lg-block" ng-if="($index+1) % 6 == 0"></div>
        <div class="clearfix visible-md-block" ng-if="($index+1) % 3 == 0"></div>
        <div class="clearfix visible-sm-block" ng-if="($index+1) % 2 == 0"></div>
    </div>
</div> 

I find clear and concise to have row management logic outside of the main repeat block. Separation of concerns :-)

Guillaume Morin
  • 3,910
  • 6
  • 25
  • 40
2

A bit late answer but i used this and i belive it is better for some cases. You can use Angular Filter package and its ChunkBy filter for this. Although this package would be a heavy lifting for this single task, there is other useful filters in it for different tasks. The code i used is like this:

<div class="row mar-t2" ng-repeat="items in posts | chunkBy:3">
    <div class="col-md-4" ng-repeat="post in items">
        <img ng-src="{{post.featured_url}}" class="img-responsive" />
        <a ng-click="modalPop(post.id)"><h1 class="s04-bas2">{{post.title.rendered}}</h1></a>
        <div class="s04-spotbox2" ng-bind-html="post.excerpt.rendered"></div>
    </div>
</div>
heirenton
  • 93
  • 3
0

I took a slightly different method using ngInit. I'm not sure if this is the appropriate solution since the ngInit documentation states

The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.

I'm not really sure if this falls under that case, but I wanted to move this functionality away from the controller to give the template designer easier freedom to group by rows with bootstrap. I still haven't tested this for binding, but seeing as i'm tracking by $index I don't think that should be a problem.

Would love to hear feedback.

I created a filter called "splitrow" that takes one argument (the count of how many items in each row)

.filter('splitrow', function(){
    return function (input, count){
        var out = [];
            if(typeof input === "object"){
                for (var i=0, j=input.length; i < j; i+=count) {
                    out.push(input.slice(i, i+count));
                }
            }
        return out;
    }
});

Within the view template I organized the bootstrap rows as follows:

<div ng-init="rows = (items|splitrow:3)">
    <div ng-repeat='row in rows' class="row">
        <div ng-repeat="item in row track by $index" class="col-md-4">
            {{item.property}}
        </div>
    </div>
</div>

I edited @Shivam's Plunker to use this method. It requires no external libraries.

Plunker

user1943442
  • 198
  • 4
  • 17
  • I like this solution. But, I can't get it to work with a data coming from a factory because `ng-init` runs first and the data isn't ready yet. It works fine if the data is a simple array on $scope. Any ideas? – WordPress Mike Jun 19 '15 at 19:19
  • @WonderBred Can you show an example in plunker of it not working? I have it working with a factory on my app that's still in development. I only really use it in 2 places so I haven't run into any issues where the directive loads before the data. – user1943442 Jun 19 '15 at 20:50
  • I'll try to get a plunker together that uses `$http`. I have a feeling that's why its not working. Are you using `$http` in your factory? Thanks for your response. – WordPress Mike Jun 19 '15 at 21:55
  • @WonderBred I use $resource in my factory. But still not sure if that makes a difference. Very interested in your use case. I'm still new with angular. – user1943442 Jun 20 '15 at 22:05
  • Here is a codepen illustrating the issue I'm having. It works fine with a hard coded array but not with data from the factory. Incidentally, is there any way to change the count later? For example, I'd like to fire a function on resize at some point that changes the column count. I'm not sure if this will be possible with ng-init. Heres the codepen. You can uncomment the array to see that the filter is working. http://codepen.io/mdmoore/pen/Qbqmoo?editors=101 – WordPress Mike Jun 21 '15 at 09:16
0

My solution is very similar to @CodeExpress one. I made a batch filter that groups items of an array (the name is borrowed it from Twig's counterpart filter). I don't handle associative arrays for simplicity's sake.

angular.module('myapp.filters', [])
    .filter('batch', function() {
        var cacheInputs = [];
        var cacheResults = [];

        return function(input, size) {
            var index = cacheInputs.indexOf(input);

            if (index !== -1) {
                return cacheResults[index];
            }

            var result = [];

            for (i = 0; i < input.length; i += size) {
                result.push(input.slice(i, i + size));
            }

            cacheInputs.push(input);
            cacheResults.push(result);

            return result;
        }
    })
;

It can be used this way:

<div ng-repeat="itemsRow in items|batch:3">
    <div class="row">
        <div ng-repeat="item in itemsRow">
            <div class="col-md-4">
                ...
            </div>
        </div>
    </div>
</div>

The filter results are cached to avoid the 10 $digest() iterations reached. Aborting! error.

Michaël Perrin
  • 5,903
  • 5
  • 40
  • 65
  • I just made a mistake by [rejecting](/review/suggested-edits/10024161) a proposed edit of this answer by @EmmanuelVerlynde. When [editing his own post](/revisions/33346976/2) @MichaëlPerrin missed out to replace one occurrence of variable `cache` with `cacheInputs`. I just did an edit to correct this. Anyways, kudos to @EmmanuelVerlynde fot spotting this error. – altocumulus Oct 29 '15 at 12:29
  • Oops sorry, I forgot to rename my variable everywhere; `cacheInputs` should be used indeed. Thanks @EmmanuelVerlynde and @altocumulus ! – Michaël Perrin Oct 29 '15 at 14:05
0

An Angular2 version of @CodeExpress's pure angular solution.

alphabet: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
                   'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

<div *ngIf='alphabet && alphabet.length'>
    <div *ngFor='#letter of alphabet; #i = index' >
        <div class="row" *ngIf='i % 4 == 0'>
            <div class="col-md-3" *ngFor='#n of [i,i+1,i+2,i+3]'>
                {{alphabet[n]}}
            </div>
        </div>
    </div>
</div>
Alex Logan
  • 1,221
  • 3
  • 18
  • 27
0

This should work

<div ng-repeat="item in items">
    <div ng-class="{row : ($index % 3 == 0)}">
        ... 
    </div>
</div>
marcinowski
  • 349
  • 2
  • 4
0

Update for Angular9:

<div class="container-fluid" *ngFor="let item of alphabet; let index = index">
  <div class="row" *ngIf="index % 4 == 0">
    <div *ngFor="let i of [index, index+1, index+2, index+3]">
      <div class="col-xs-3">{{alphabet[i]}}</div>
    </div>
  </div>
</div>