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>
2
  • 3
    Will be better if you put a plnkr.co with current code Commented Nov 22, 2013 at 5: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 here. Commented Sep 14, 2014 at 21:19

11 Answers 11

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.

Sign up to request clarification or add additional context in comments.

12 Comments

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.
@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 plnkr.co/edit/L207RC5Dmxk61fusC2rr?p=preview
ok I found a solution, you need to use 'as': item in items | filter:x as results @see docs.angularjs.org/api/ng/directive/ngRepeat
@user1943442, can you elaborate on how the method 2 doesn't work ? The code in plunker is working
Just a heads up, groupBy is now chunkBy: n in angular-filter
|
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.

4 Comments

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
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.
I think "item" within the inner div needs to be in double-curly-braces, no? {{item}}
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!
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!

1 Comment

I like this approach!
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).

1 Comment

clever, dirty, and affective. +1
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 :-)

1 Comment

Thanks, first solution that worked perfectly for me after working on this for several hours.
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>

Comments

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

5 Comments

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?
@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.
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.
@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.
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. codepen.io/mdmoore/pen/Qbqmoo?editors=101
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.

2 Comments

I just made a mistake by rejecting a proposed edit of this answer by @EmmanuelVerlynde. When editing his own post @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.
Oops sorry, I forgot to rename my variable everywhere; cacheInputs should be used indeed. Thanks @EmmanuelVerlynde and @altocumulus !
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>

Comments

0

This should work

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

Comments

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>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.