Angular ng-repeat add bootstrap row every 3 or 4 cols
Asked Answered
E

18

113

I'm looking for the right pattern to inject a bootstrap row class every each 3 columns. I need this because cols doesn't have a fixed hight (and I don't want to fix one), so it breaks my design !

Here is my code :

<div ng-repeat="product in products">
    <div ng-if="$index % 3 == 0" class="row">
        <div class="col-sm-4" >
            ...
        </div>
    </div>
</div>

But it does only display one product in each row. What I want as final result is :

<div class="row">
    <div class="col-sm4"> ... </div>
    <div class="col-sm4"> ... </div>
    <div class="col-sm4"> ... </div>
</div>
<div class="row">
    <div class="col-sm4"> ... </div>
    <div class="col-sm4"> ... </div>
    <div class="col-sm4"> ... </div>
</div>

Can I achieve this with only ng-repeat pattern (without directive or controller) ? The docs introduce ng-repeat-start and ng-repeat-end but I can't figure out how to use it is this use case ! I feel like this is something we often use in bootstrap templating ! ? Thanks

Eurythermal answered 30/11, 2014 at 9:24 Comment(6)
I think you should model your data in a way that fits your design, it should probably be multidimensional array or object, with representation of rows and columns, then you should iterate over rows and use conditional class "ng-class" directive and inside row you should then iterate over columns.Lillith
Interesting and certainly a working solution but the day I want to display 4 products a row instead of 3, I have, to modify my data structure, I would prefer this to stay in the "scope" of pure display functionality ...Eurythermal
I see, then you should probobly iterate in chunks as in Ariel answer, also you may find this post #18565388 usefull.Lillith
I think this is exactly what you're looking for: https://mcmap.net/q/183157/-angular-js-ng-repeat-for-creating-gridForester
related https://mcmap.net/q/183157/-angular-js-ng-repeat-for-creating-gridBanky
github.com/jeevasusej/bootstrapRowSplitterMarshallmarshallese
A
166

The top voted answer, while effective, is not what I would consider to be the angular way, nor is it using bootstrap's own classes that are meant to deal with this situation. As @claies mentioned, the .clearfix class is meant for situations such as these. In my opinion, the cleanest implementation is as follows:

<div class="row">
    <div ng-repeat="product in products">
        <div class="clearfix" ng-if="$index % 3 == 0"></div>
        <div class="col-sm-4">
            <h2>{{product.title}}</h2>
        </div>
    </div>
</div>

This structure avoids messy indexing of the products array, allows for clean dot notation, and makes use of the clearfix class for its intended purpose.

Asymptotic answered 2/9, 2015 at 16:8 Comment(9)
This is a good idea, however if you are interested in using flexbox you have to use this on the row and not on the divs inside of the rows in order to allow each box/div to be the same height. Clearfix is great but does not help with keeping everything lined up.Borderland
Hacking it by using objects[$index] ... objects[$index + 1] didn't work for me since using a flexbox prevented multiple columns from going to different rows on smaller screens, but this worked out. THANKS!Hypocoristic
codepen link Thanks. This helped me with an idea to solve my own problem... Cards with arbitrary heights displayed in a bootstrap grid using different column amounts at each resolution... Combining the clearfix with a little $index math trickery.Pisarik
I'm having hard time understanding how data array should be structured to make that work. what is wrong (jsfiddle.net/danny3b/Lvc0u55v/11503)?Tergiversate
@Tergiversate your angular code is not valid, and you may want to use a more modern version (i.e. > 1.3)Asymptotic
if it was valid, I wouldn't ask :)Tergiversate
How could it be done when I have only one product and I want to set this product to the middle col or center its col. So later when I register more products they assume the original 3 cols format?Radiomicrometer
@Radiomicrometer there are multiple ways to handle that. Off the top of my head, I'd probably put conditional ng-if statements on the second child div of the ng-repeat. Something like, <div class="col-sm-4" ng-if="products.length > 1"> with another <div class="col-sm-12" ng-if="products.length == 1">. This would work as long as you're not iterating over hundreds of items (and therefore creating hundreds of watches)Asymptotic
This was a lifesaver today, years after the answer was written. Thanks StackOverflow!Lido
A
149

I know it's a bit late but it still might help someone. I did it like this:

<div ng-repeat="product in products" ng-if="$index % 3 == 0" class="row">
    <div class="col-xs-4">{{products[$index]}}</div>
    <div class="col-xs-4" ng-if="products.length > ($index + 1)">{{products[$index + 1]}}</div>
    <div class="col-xs-4" ng-if="products.length > ($index + 2)">{{products[$index + 2]}}</div>
</div>

jsfiddle

Antennule answered 15/5, 2015 at 12:22 Comment(10)
Great solution, however it doesn't check whether $index + 1 and $index +2 are past the array boundaries. The last two divs require ng-if="$index+1 < products.length" and ng-if="$index+2 < products.length"Bakke
Can we do this for key,value pair. getting next key ? Instead of getting $index+1Revisal
I am only seeing those items that are multiples of three so I only see the items with the index of 0, 3, 6, etc. This doesn't show all of the items.Borderland
What doesnt work for me is that I can sort my list anymore... is there a solution for this?Coeval
I combined this answer with setting a variable (https://mcmap.net/q/183158/-set-angular-scope-variable-in-markup) to simplify syntax: {{product=products[$index];""}}Andromada
What is the solution for sorting, if we applied any filter on products .Hephaestus
@CodedContainer you need to repeat the same template three times and get non-multiples using $index +1 and so on. Note in his example he creates 3 divs, for $index, then $index +1 and $index + 2.Boundless
Nice one, helped allot, now just gotta figure out equa distant column and row gaps : ' )Commuter
Seems not working when using orderBy method. Is there a way to make it work?Satinet
Here is my jsfiddle example using orderBy: jsfiddle.net/claudchan/31rtqvt4Satinet
W
25

Okay, this solution is far simpler than the ones already here, and allows different column widths for different device widths.

<div class="row">
    <div ng-repeat="image in images">
        <div class="col-xs-6 col-sm-4 col-md-3 col-lg-2">
            ... your content here ...
        </div>
        <div class="clearfix visible-lg" ng-if="($index + 1) % 6 == 0"></div>
        <div class="clearfix visible-md" ng-if="($index + 1) % 4 == 0"></div>
        <div class="clearfix visible-sm" ng-if="($index + 1) % 3 == 0"></div>
        <div class="clearfix visible-xs" ng-if="($index + 1) % 2 == 0"></div>
    </div>
</div>

Note that the % 6 part is supposed to equal the number of resulting columns. So if on the column element you have the class col-lg-2 there will be 6 columns, so use ... % 6.

This technique (excluding the ng-if) is actually documented here: Bootstrap docs

Whereupon answered 25/11, 2015 at 20:27 Comment(4)
In my opinion, this is the best solution.Sillsby
If new to bootstrap, it is easy to overlook the fact that defining the rows is not required. This worked perfectly and is a slightly more complete version of Duncan's solution.Schreibman
This is exactly what I was looking for.Winglet
@Schreibman What is the advantage of this over Duncan's?Bradski
P
17

While what you want to accomplish may be useful, there is another option which I believe you might be overlooking that is much more simple.

You are correct, the Bootstrap tables act strangely when you have columns which are not fixed height. However, there is a bootstrap class created to combat this issue and perform responsive resets.

simply create an empty <div class="clearfix"></div> before the start of each new row to allow the floats to reset and the columns to return to their correct positions.

here is a bootply.

Petroleum answered 30/11, 2014 at 17:16 Comment(2)
This doesn't solve the negative 15px of margin that each .row has for bootstrap.Perquisite
Does this work with flex to make columns the same height?Boundless
E
16

Thanks for your suggestions, you got me on the right way !

Let's go for a complete explanation :

  • By default AngularJS http get query returns an object

  • So if you want to use @Ariel Array.prototype.chunk function you have first to transform object into an array.

  • And then to use the chunk function IN YOUR CONTROLLER otherwise if used directly into ng-repeat, it will brings you to an infdig error. The final controller looks :

    // Initialize products to empty list
    $scope.products = [];
    
    // Load products from config file
    $resource("/json/shoppinglist.json").get(function (data_object)
    {
        // Transform object into array
        var data_array =[];
        for( var i in data_object ) {
            if (typeof data_object[i] === 'object' && data_object[i].hasOwnProperty("name")){
                data_array.push(data_object[i]);
            }
        }
        // Chunk Array and apply scope
        $scope.products = data_array.chunk(3);
    });
    

And HTML becomes :

<div class="row" ng-repeat="productrow in products">

    <div class="col-sm-4" ng-repeat="product in productrow">

On the other side, I decided to directly return an array [] instead of an object {} from my JSON file. This way, controller becomes (please note specific syntax isArray:true) :

    // Initialize products to empty list 
    $scope.products = [];

    // Load products from config file
    $resource("/json/shoppinglist.json").query({method:'GET', isArray:true}, function (data_array)
    {
        $scope.products = data_array.chunk(3);
    });

HTML stay the same as above.

OPTIMIZATION

Last question in suspense is : how to make it 100% AngularJS without extending javascript array with chunk function ... if some people are interested in showing us if ng-repeat-start and ng-repeat-end are the way to go ... I'm curious ;)

ANDREW'S SOLUTION

Thanks to @Andrew, we now know adding a bootstrap clearfix class every three (or whatever number) element corrects display problem from differents block's height.

So HTML becomes :

<div class="row">

    <div ng-repeat="product in products">

        <div ng-if="$index % 3 == 0" class="clearfix"></div>

        <div class="col-sm-4"> My product descrition with {{product.property}}

And your controller stays quite soft with removed chunck function :

// Initialize products to empty list 
        $scope.products = [];

        // Load products from config file
        $resource("/json/shoppinglist.json").query({method:'GET', isArray:true}, function (data_array)
        {
            //$scope.products = data_array.chunk(3);
            $scope.products = data_array;
        });
Eurythermal answered 30/11, 2014 at 17:4 Comment(0)
C
7

You can do it without a directive but i'm not sure it's the best way. To do this you must create array of array from the data you want to display in the table, and after that use 2 ng-repeat to iterate through the array.

to create the array for display use this function like that products.chunk(3)

Array.prototype.chunk = function(chunkSize) {
    var array=this;
    return [].concat.apply([],
        array.map(function(elem,i) {
            return i%chunkSize ? [] : [array.slice(i,i+chunkSize)];
        })
    );
}

and then do something like that using 2 ng-repeat

<div class="row" ng-repeat="row in products.chunk(3)">
  <div class="col-sm4" ng-repeat="item in row">
    {{item}}
  </div>
</div>
Cysticercus answered 30/11, 2014 at 10:28 Comment(0)
I
7

Based on Alpar solution, using only templates with anidated ng-repeat. Works with both full and partially empty rows:

<div data-ng-app="" data-ng-init="products='soda','beer','water','milk','wine']" class="container">
    <div ng-repeat="product in products" ng-if="$index % 3 == 0" class="row">
        <div class="col-xs-4" 
            ng-repeat="product in products.slice($index, ($index+3 > products.length ? 
            products.length : $index+3))"> {{product}}</div>
    </div>
</div>

JSFiddle

Inseparable answered 10/12, 2015 at 14:2 Comment(0)
D
6

I've just made a solution of it working only in template. The solution is

    <span ng-repeat="gettingParentIndex in products">
        <div class="row" ng-if="$index<products.length/2+1">    <!-- 2 columns -->
            <span ng-repeat="product in products">
                <div class="col-sm-6" ng-if="$index>=2*$parent.$index && $index <= 2*($parent.$index+1)-1"> <!-- 2 columns -->
                    {{product.foo}}
                </div>
            </span>
        </div>
    </span>

Point is using data twice, one is for an outside loop. Extra span tags will remain, but it depends on how you trade off.

If it's a 3 column layout, it's going to be like

    <span ng-repeat="gettingParentIndex in products">
        <div class="row" ng-if="$index<products.length/3+1">    <!-- 3 columns -->
            <span ng-repeat="product in products">
                <div class="col-sm-4" ng-if="$index>=3*$parent.$index && $index <= 3*($parent.$index+1)-1"> <!-- 3 columns -->
                    {{product.foo}}
                </div>
            </span>
        </div>
    </span>

Honestly I wanted

$index<Math.ceil(products.length/3)

Although it didn't work.

Derril answered 6/2, 2015 at 23:27 Comment(7)
I tried this solution to implement 2 elements in each row. For example, i have 5 elements in a list, so the output i should have is 3 rows with 2 elements/columns in first 2 rows and 1 column in last row. The problem is, i am getting 5 rows here with last 2 rows being empty. Wondering how to fix this? ThanksEspionage
@MaverickAzy thanks for trying. I know there is an issue that if height of those elements is different it doesn't work well.Derril
The height of the elements are actually the same. The problem is , i should get only 3 rows but getting 5 rows with last 2 empty rows. Can you tell me if products.length is 5, then 5/2+1 = ? This logic is not clear for me at line no.2 for class row.Espionage
@MaverickAzy those empty rows must be generated as they are. Does it mess your layout?Derril
no, it does not mess the layout. Only concern is regarding the empty rows. Really appreciate if you can help me with this. ThanksEspionage
@MaverickAzy I'm sorry, but that is how this is. This logic generate the empty row on purpose.Derril
Let us continue this discussion in chat.Espionage
E
5

Just another little improvement about @Duncan answer and the others answers based on clearfix element. If you want to make the content clickable you will need a z-index > 0 on it or clearfix will overlap the content and handle the click.

This is the example not working (you can't see the cursor pointer and clicking will do nothing):

<div class="row">
    <div ng-repeat="product in products">
        <div class="clearfix" ng-if="$index % 3 == 0"></div>
        <div class="col-sm-4" style="cursor: pointer" ng-click="doSomething()">
            <h2>{{product.title}}</h2>
        </div>
    </div>
</div>

While this is the fixed one:

<div class="row">
    <div ng-repeat-start="product in products" class="clearfix" ng-if="$index % 3 == 0"></div>
    <div ng-repeat-end class="col-sm-4" style="cursor: pointer; z-index: 1" ng-click="doSomething()">
            <h2>{{product.title}}</h2>
    </div>
</div>

I've added z-index: 1 to have the content raise over the clearfix and I've removed the container div using instead ng-repeat-start and ng-repeat-end (available from AngularJS 1.2) because it made z-index not working.

Hope this helps!

Update

Plunker: http://plnkr.co/edit/4w5wZj

Epitasis answered 18/5, 2016 at 8:42 Comment(4)
Does this work with flex in rows to make columns the same height?Boundless
I'm not sure I understand your question. This is a fast plunker to show you what this code do: plnkr.co/edit/4w5wZj?p=preview. In words, clearfix correctly align the second line of titles: they all start from the same point but they still don't have the same height (as you can see thanks to background color). Try to delete clearfix class to see what is the default behaviour. I've used flexbox only one or two times but it has a lot of css properties and I'm sure you can find what you're searching.Epitasis
bootstrap provides an example on how to make all columns in same row to get the same height of the tallest column. I had to use this. The problem is it looses the ability to wrap onto a new line when there are more than 12 columns, so you need to manually create new rows. After researching a bit more, I could get a solution and posted here as answer, though I don't know if it's the best one. Thanks anyway, your answer helped me!Boundless
It is the first time I see that example, it's really useful! Glad to help you.Epitasis
S
4

i solved this using ng-class

<div ng-repeat="item in items">
    <div ng-class="{ 'row': ($index + 1) % 4 == 0 }">
        <div class="col-md-3">
            {{item.name}}
        </div>
    </div>
</div>
Sowell answered 29/6, 2016 at 7:59 Comment(0)
M
2

The best way to apply a class is to use ng-class.It can be used to apply classes based on some condition.

<div ng-repeat="product in products">
   <div ng-class="getRowClass($index)">
       <div class="col-sm-4" >
           <!-- your code -->
       </div>
   </div>

and then in your controller

$scope.getRowClass = function(index){
    if(index%3 == 0){
     return "row";
    }
}
Mythos answered 28/4, 2016 at 2:8 Comment(0)
B
2

After combining many answers and suggestion here, this is my final answer, which works well with flex, which allows us to make columns with equal height, it also checks the last index, and you don't need to repeat the inner HTML. It doesn't use clearfix:

<div ng-repeat="prod in productsFiltered=(products | filter:myInputFilter)" ng-if="$index % 3 == 0" class="row row-eq-height">
    <div ng-repeat="i in [0, 1, 2]" ng-init="product = productsFiltered[$parent.$parent.$index + i]"  ng-if="$parent.$index + i < productsFiltered.length" class="col-xs-4">
        <div class="col-xs-12">{{ product.name }}</div>
    </div>
</div>

It will output something like this:

<div class="row row-eq-height">
    <div class="col-xs-4">
        <div class="col-xs-12">
            Product Name
        </div>
    </div>
    <div class="col-xs-4">
        <div class="col-xs-12">
            Product Name
        </div>
    </div>
    <div class="col-xs-4">
        <div class="col-xs-12">
            Product Name
        </div>
    </div>
</div>
<div class="row row-eq-height">
    <div class="col-xs-4">
        <div class="col-xs-12">
            Product Name
        </div>
    </div>
    <div class="col-xs-4">
        <div class="col-xs-12">
            Product Name
        </div>
    </div>
    <div class="col-xs-4">
        <div class="col-xs-12">
            Product Name
        </div>
    </div>
</div>
Boundless answered 9/8, 2016 at 18:14 Comment(0)
Q
1

Little bit modification in @alpar 's solution

<div data-ng-app="" data-ng-init="products=['A','B','C','D','E','F', 'G','H','I','J','K','L']" class="container">
    <div ng-repeat="product in products" ng-if="$index % 6 == 0" class="row">
        <div class="col-xs-2" ng-repeat="idx in [0,1,2,3,4,5]">
        {{products[idx+$parent.$index]}} <!-- When this HTML is Big it's useful approach -->
        </div>
    </div>
</div>

jsfiddle

Quatre answered 4/9, 2015 at 21:49 Comment(0)
C
0

This worked for me, no splicing or anything required:

HTML

<div class="row" ng-repeat="row in rows() track by $index">
    <div class="col-md-3" ng-repeat="item in items" ng-if="indexInRange($index,$parent.$index)"></div>
</div>

JavaScript

var columnsPerRow = 4;
$scope.rows = function() {
  return new Array(columnsPerRow);
};
$scope.indexInRange = function(columnIndex,rowIndex) {
  return columnIndex >= (rowIndex * columnsPerRow) && columnIndex < (rowIndex * columnsPerRow) + columnsPerRow;
};
Courtenay answered 31/8, 2016 at 19:55 Comment(0)
F
0

Born Solutions its best one, just need a bit tweek to feet the needs, i had different responsive solutions and changed a bit

<div ng-repeat="post in posts">
    <div class="vechicle-single col-lg-4 col-md-6 col-sm-12 col-xs-12">
    </div>
    <div class="clearfix visible-lg" ng-if="($index + 1) % 3 == 0"></div>
    <div class="clearfix visible-md" ng-if="($index + 1) % 2 == 0"></div>
    <div class="clearfix visible-sm" ng-if="($index + 1) % 1 == 0"></div>
    <div class="clearfix visible-xs" ng-if="($index + 1) % 1 == 0"></div>
</div>
Florencia answered 30/9, 2016 at 22:16 Comment(0)
O
0

Building on Alpar's answer, here's a more generalised way to split a single list of items into multiple containers (rows, columns, buckets, whatever):

<div class="row" ng-repeat="row in [0,1,2]">
  <div class="col" ng-repeat="item in $ctrl.items" ng-if="$index % 3 == row">
    <span>{{item.name}}</span>
  </div>
</div> 

for a list of 10 items, generates:

<div class="row">
  <div class="col"><span>Item 1</span></div>
  <div class="col"><span>Item 4</span></div>
  <div class="col"><span>Item 7</span></div>
  <div class="col"><span>Item 10</span></div>
</div> 
<div class="row">
  <div class="col"><span>Item 2</span></div>
  <div class="col"><span>Item 5</span></div>
  <div class="col"><span>Item 8</span></div>
</div> 
<div class="row">
  <div class="col"><span>Item 3</span></div>
  <div class="col"><span>Item 6</span></div>
  <div class="col"><span>Item 9</span></div>
</div> 

The number of containers can be quickly coded into a controller function:

JS (ES6)

$scope.rowList = function(rows) {
  return Array(rows).fill().map((x,i)=>i);
}
$scope.rows = 2;

HTML

<div class="row" ng-repeat="row in rowList(rows)">
  <div ng-repeat="item in $ctrl.items" ng-if="$index % rows == row">
    ...

This approach avoids duplicating the item markup (<span>{{item.name}}</span> in this case) in the source template - not a huge win for a simple span, but for a more complex DOM structure (which I had) this helps keep the template DRY.

Oestrone answered 31/10, 2016 at 15:24 Comment(0)
S
0

Update 2019 - Bootstrap 4

Since Bootstrap 3 used floats, it required clearfix resets every n (3 or 4) columns (.col-*) in the .row to prevent uneven wrapping of columns.

Now that Bootstrap 4 uses flexbox, there is no longer a need to wrap columns in separate .row tags, or to insert extra divs to force cols to wrap every n columns.

You can simply repeat all of the columns in a single .row container.

For example 3 columns in each visual row is:

<div class="row">
     <div class="col-4">...</div>
     <div class="col-4">...</div>
     <div class="col-4">...</div>
     <div class="col-4">...</div>
     <div class="col-4">...</div>
     <div class="col-4">...</div>
     <div class="col-4">...</div>
     (...repeat for number of items)
</div>

So for Bootstrap the ng-repeat is simply:

  <div class="row">
      <div class="col-4" ng-repeat="item in items">
          ... {{ item }}
      </div>
  </div>

Demo: https://www.codeply.com/go/Z3IjLRsJXX

Sequence answered 11/3, 2019 at 11:41 Comment(0)
G
0

I did it only using boostrap, you must be very careful in the location of the row and the column, here is my example.

<section>
<div class="container">
        <div ng-app="myApp">
        
                <div ng-controller="SubregionController">
                    <div class="row text-center">
                        <div class="col-md-4" ng-repeat="post in posts">
                            <div >
                                <div>{{post.title}}</div>
                            </div>
                        </div>
                    
                    </div>
                </div>        
        </div>
    </div>
</div> 

</section>
Griffey answered 12/6, 2019 at 16:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.