149

I need to use ng-repeat (in AngularJS) to list all of the elements in an array.

The complication is that each element of the array will transform to either one, two or three rows of a table.

I cannot create valid html, if ng-repeat is used on an element, as no type of repeating element is allowed between <tbody> and <tr>.

For example, if I used ng-repeat on <span>, I would get:

<table>
  <tbody>
    <span>
      <tr>...</tr>
    </span>
    <span>
      <tr>...</tr>
      <tr>...</tr>
      <tr>...</tr>
    </span>
    <span>
      <tr>...</tr>
      <tr>...</tr>
    </span>
  </tbody>
</table>          

Which is invalid html.

But what I need to be generated is:

<table>
  <tbody>
    <tr>...</tr>
    <tr>...</tr>
    <tr>...</tr>
    <tr>...</tr>
    <tr>...</tr>
    <tr>...</tr>
  </tbody>
</table>          

where the first row has been generated by the first array element, the next three by the second and the fifth and sixth by the last array element.

How can I use ng-repeat in such a way that the html element to which it is bound 'disappears' during rendering?

Or is there another solution to this?


Clarification: The generated structure should look like below. Each array element can generate between 1-3 rows of the table. The answer should ideally support 0-n rows per array element.

<table>
  <tbody>
    <!-- array element 0 -->
    <tr>
      <td>One row item</td>
    </tr>
    <!-- array element 1 -->
    <tr>
      <td>Three row item</td>
    </tr>
    <tr>
      <td>Some product details</td>
    </tr>
    <tr>
      <td>Customer ratings</td>
    </tr>
    <!-- array element 2 -->
    <tr>
      <td>Two row item</td>
    </tr>
    <tr>
      <td>Full description</td>
    </tr>
  </tbody>
</table>          
fadedbee
  • 42,671
  • 44
  • 178
  • 308
  • Maybe you should use "replace: true"? See this: http://stackoverflow.com/questions/11426114/angularjs-why-doesnt-replace-true-work-with-templateurl-property –  Jul 15 '12 at 11:40
  • Also, why can't you use ng-repeat on the tr itself? –  Jul 15 '12 at 12:55
  • 2
    @Tommy, because "each element of the array will transform to either one, two or three rows of a table". If I used ng-repeat on the `tr` I would get one row per array element, as far as I understand. – fadedbee Jul 15 '12 at 13:37
  • 1
    Ok, I see. Can't you just flatten the model before you use it in the repeater? –  Jul 15 '12 at 14:57
  • @Tommy, no. The 1-3 `tr`s which are generated by one array element do not have the same structure. – fadedbee Jul 15 '12 at 15:09
  • Could you add a bit of json showing the structure of the items? I'm hoping for [{title:"", description:""},{title:"Foo", description:"D",ratings:"asdf"}] But some answers are assuming a nested array. – iwein Dec 27 '12 at 05:06
  • I know this is old, but can't you just use ng-repeat on the element to repeat, not it's container? Or wasn't this available in 2012? [fiddle here](http://jsfiddle.net/u2cgg34q/1/). – thinkOfaNumber May 28 '15 at 01:31

8 Answers8

71

As of AngularJS 1.2 there's a directive called ng-repeat-start that does exactly what you ask for. See my answer in this question for a description of how to use it.

Community
  • 1
  • 1
jmagnusson
  • 5,799
  • 4
  • 43
  • 38
68

Update: If you are using Angular 1.2+, use ng-repeat-start. See @jmagnusson's answer.

Otherwise, how about putting the ng-repeat on tbody? (AFAIK, it is okay to have multiple <tbody>s in a single table.)

<tbody ng-repeat="row in array">
  <tr ng-repeat="item in row">
     <td>{{item}}</td>
  </tr>
</tbody>
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 64
    While this might technically work, it's very disappointing that the answer for this common use case is that you have to inject arbitrary (otherwise unnecessary) markup. I have the same problem (repeated groups of rows -- one header TR with one or more child TRs, repeated as a group). Trivial with other template engines, hacky at best with Angular it seems. – Brian Moeskau Jun 17 '13 at 07:53
  • 1
    Agreed. I am trying to do a repeat on the DT+DD elements. there's no way in doing that without adding a invalid wrapping element – David Lin Jul 18 '13 at 00:18
  • 2
    @DavidLin, in Angular v1.2 (whenever it comes out) you'll be able to repeat over multiple elements, e.g., dt and dd: http://www.youtube.com/watch?v=W13qDdJDHp8&t=17m28s – Mark Rajcok Jul 18 '13 at 13:50
  • This is handled elegantly in KnockoutJS using containerless control flow syntax: http://knockoutjs.com/documentation/foreach-binding.html. Hope to see Angular do something similar soon. – Cory House Apr 28 '14 at 18:49
  • 3
    This answer is outdated, use ng-repeat-start instead – iwein Jun 23 '14 at 22:51
  • @bmoeskau I think this issue almost does not matter because at least one `tbody` will be added automatically anyway. – ivkremer Aug 21 '14 at 17:00
  • @Kremchik The point is not that it's not possible (hacky), it's that if your template engine is forcing me to add additional (and unnecessary) markup into my output to support its looping capabilities, you're probably doing it wrong. Besides, this seems to have been solved based on other comments (I wouldn't know, I never got beyond these goofy issues early on) so not sure why this is relevant now. – Brian Moeskau Aug 21 '14 at 17:12
  • @bmoeskau Yeah, I understand. I had a bit different issue, though. I had `` construction. But I wanted each item followed by its children: ``. So I considered a hack with `tbody` was a best solution. – ivkremer Aug 21 '14 at 17:36
40

If you use ng > 1.2, here is an example of using ng-repeat-start/end without generating unnecessary tags:

<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script>
      angular.module('mApp', []);
    </script>
  </head>
  <body ng-app="mApp">
    <table border="1" width="100%">
      <tr ng-if="0" ng-repeat-start="elem in [{k: 'A', v: ['a1','a2']}, {k: 'B', v: ['b1']}, {k: 'C', v: ['c1','c2','c3']}]"></tr>

      <tr>
        <td rowspan="{{elem.v.length}}">{{elem.k}}</td>
        <td>{{elem.v[0]}}</td>
      </tr>
      <tr ng-repeat="v in elem.v" ng-if="!$first">
        <td>{{v}}</td>
      </tr>

      <tr ng-if="0" ng-repeat-end></tr>
    </table>
  </body>
</html>

The important point: for tags used for ng-repeat-start and ng-repeat-end set ng-if="0", to let not be inserted in the page. In this way the inner content will be handled exactly as it is in knockoutjs (using commands in <!--...-->), and there will be no garbage.

Duka Árpád
  • 421
  • 4
  • 3
20

You might want to flatten the data within your controller:

function MyCtrl ($scope) {
  $scope.myData = [[1,2,3], [4,5,6], [7,8,9]];
  $scope.flattened = function () {
    var flat = [];
    $scope.myData.forEach(function (item) {
      flat.concat(item);
    }
    return flat;
  }
}

And then in the HTML:

<table>
  <tbody>
    <tr ng-repeat="item in flattened()"><td>{{item}}</td></tr>
  </tbody>
</table>
btford
  • 5,631
  • 2
  • 29
  • 26
  • 1
    No it's not. Calling a function inside a ngRepeat expression is absolutely not recommended – Nik Aug 04 '15 at 09:53
  • In my case, I did need to add a separator for each element where the index != 0 and ```ng-repeat-start```, ```ng-repeat-end``` break my design, so the solution was to create an extra variable adding this separator object before each element and iterate the new var: ```
    ```
    – hermeslm Jan 06 '20 at 18:09
10

The above is correct but for a more general answer it is not enough. I needed to nest ng-repeat, but stay on the same html level, meaning write the elements in the same parent. The tags array contain tag(s) that also have a tags array. It is actually a tree.

[{ name:'name1', tags: [
  { name: 'name1_1', tags: []},
  { name: 'name1_2', tags: []}
  ]},
 { name:'name2', tags: [
  { name: 'name2_1', tags: []},
  { name: 'name2_2', tags: []}
  ]}
]

So here is what I eventually did.

<div ng-repeat-start="tag1 in tags" ng-if="false"></div>
    {{tag1}},
  <div ng-repeat-start="tag2 in tag1.tags" ng-if="false"></div>
    {{tag2}},
  <div ng-repeat-end ng-if="false"></div>
<div ng-repeat-end ng-if="false"></div>

Note the ng-if="false" that hides the start and end divs.

It should print

name1,name1_1,name1_2,name2,name2_1,name2_2,

agelbess
  • 4,249
  • 3
  • 20
  • 21
1

I would like to just comment, but my reputation is still lacking. So i'm adding another solution which solves the problem as well. I would really like to refute the statement made by @bmoeskau that solving this problem requires a 'hacky at best' solution, and since this came up recently in a discussion even though this post is 2 years old, i'd like to add my own two cents:

As @btford has pointed out, you seem to be trying to turn a recursive structure into a list, so you should flatten that structure into a list first. His solution does that, but there is an opinion that calling the function inside the template is inelegant. if that is true (honestly, i dont know) wouldnt that just require executing the function in the controller rather than the directive?

either way, your html requires a list, so the scope that renders it should have that list to work with. you simply have to flatten the structure inside your controller. once you have a $scope.rows array, you can generate the table with a single, simple ng-repeat. No hacking, no inelegance, simply the way it was designed to work.

Angulars directives aren't lacking functionality. They simply force you to write valid html. A colleague of mine had a similar issue, citing @bmoeskau in support of criticism over angulars templating/rendering features. When looking at the exact problem, it turned out he simply wanted to generate an open-tag, then a close tag somewhere else, etc.. just like in the good old days when we would concat our html from strings.. right? no.

as for flattening the structure into a list, here's another solution:

// assume the following structure
var structure = [
    {
        name: 'item1', subitems: [
            {
                name: 'item2', subitems: [
                ],
            }
        ],
    }
];
var flattened = structure.reduce((function(prop,resultprop){
    var f = function(p,c,i,a){
        p.push(c[resultprop]);
        if (c[prop] && c[prop].length > 0 )
          p = c[prop].reduce(f,p);
        return p;
    }
    return f;
})('subitems','name'),[]);

// flattened now is a list: ['item1', 'item2']

this will work for any tree-like structure that has sub items. If you want the whole item instead of a property, you can shorten the flattening function even more.

hope that helps.

Ar Es
  • 389
  • 3
  • 12
0

for a solution that really works

html

<remove  ng-repeat-start="itemGroup in Groups" ></remove>
   html stuff in here including inner repeating loops if you want
<remove  ng-repeat-end></remove>

add an angular.js directive

//remove directive
(function(){
    var remove = function(){

        return {    
            restrict: "E",
            replace: true,
            link: function(scope, element, attrs, controller){
                element.replaceWith('<!--removed element-->');
            }
        };

    };
    var module = angular.module("app" );
    module.directive('remove', [remove]);
}());

for a brief explanation,

ng-repeat binds itself to the <remove> element and loops as it should, and because we have used ng-repeat-start / ng-repeat-end it loops a block of html not just an element.

then the custom remove directive places the <remove> start and finish elements with <!--removed element-->

Clint
  • 973
  • 7
  • 18
-2
<table>
  <tbody>
    <tr><td>{{data[0].foo}}</td></tr>
    <tr ng-repeat="d in data[1]"><td>{{d.bar}}</td></tr>
    <tr ng-repeat="d in data[2]"><td>{{d.lol}}</td></tr>
  </tbody>
</table>

I think that this is valid :)

Renan Tomal Fernandes
  • 10,978
  • 4
  • 48
  • 31
  • While this works, it will only work if the array has three elements. – btford Jul 15 '12 at 23:32
  • Just make sure that the array have 3 elements, even if they are empty arrays(ng-repeat with an empty array simply don't render anything). – Renan Tomal Fernandes Jul 16 '12 at 00:54
  • 7
    My point was that the OP probably wants a solution that works for a variable number of items in the array. I assume hardcoding "there must be three items in this array" into the template would be a poor solution. – btford Jul 16 '12 at 02:49
  • In the last comment on his question he says that the elements won't have the same structure, so hardcoding each structure is inevitable. – Renan Tomal Fernandes Jul 16 '12 at 03:02