How to use ng-repeat without an html element
Asked Answered
B

8

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>          
Beulabeulah answered 15/7, 2012 at 9:49 Comment(7)
Maybe you should use "replace: true"? See this: #11426614Cumuliform
Also, why can't you use ng-repeat on the tr itself?Cumuliform
@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.Beulabeulah
Ok, I see. Can't you just flatten the model before you use it in the repeater?Cumuliform
@Tommy, no. The 1-3 trs which are generated by one array element do not have the same structure.Beulabeulah
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.Stipel
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.Katzen
Z
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>
Zenobia answered 16/8, 2012 at 20:39 Comment(8)
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.Unattached
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 elementPadraig
@DavidLin, in Angular v1.2 (whenever it comes out) you'll be able to repeat over multiple elements, e.g., dt and dd: youtube.com/watch?v=W13qDdJDHp8&t=17m28sZenobia
This is handled elegantly in KnockoutJS using containerless control flow syntax: knockoutjs.com/documentation/foreach-binding.html. Hope to see Angular do something similar soon.Duchy
This answer is outdated, use ng-repeat-start insteadStipel
@bmoeskau I think this issue almost does not matter because at least one tbody will be added automatically anyway.Op
@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.Unattached
@bmoeskau Yeah, I understand. I had a bit different issue, though. I had <tr ng-repeat item in items></tr> construction. But I wanted each item followed by its children: <tr ng-repeat subitem in item></tr>. So I considered a hack with tbody was a best solution.Op
B
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.

Birecree answered 17/9, 2013 at 6:57 Comment(1)
Angular has caught up, this is the proper solution now.Stipel
Z
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>
Zenobia answered 16/8, 2012 at 20:39 Comment(8)
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.Unattached
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 elementPadraig
@DavidLin, in Angular v1.2 (whenever it comes out) you'll be able to repeat over multiple elements, e.g., dt and dd: youtube.com/watch?v=W13qDdJDHp8&t=17m28sZenobia
This is handled elegantly in KnockoutJS using containerless control flow syntax: knockoutjs.com/documentation/foreach-binding.html. Hope to see Angular do something similar soon.Duchy
This answer is outdated, use ng-repeat-start insteadStipel
@bmoeskau I think this issue almost does not matter because at least one tbody will be added automatically anyway.Op
@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.Unattached
@bmoeskau Yeah, I understand. I had a bit different issue, though. I had <tr ng-repeat item in items></tr> construction. But I wanted each item followed by its children: <tr ng-repeat subitem in item></tr>. So I considered a hack with tbody was a best solution.Op
D
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.

Danedanegeld answered 10/7, 2015 at 15:9 Comment(1)
after setting ng-if="0" , it throws error saying unable to find matching ng-repeat-end.Humbert
C
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>
Compare answered 15/7, 2012 at 19:23 Comment(2)
No it's not. Calling a function inside a ngRepeat expression is absolutely not recommendedHydantoin
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: <div class="c477_group"> <div class="c477_group_item" ng-repeat="item in itemsWithSeparator" ng-switch="item.id" ng-class="{'-divider' : item.id == 'SEPARATOR'}"> <div class="c478" ng-switch-when="FAS"/> <div class="c478" ng-switch-when="SEPARATOR" /> <div class="c479" ng-switch-default /> </div> </div>Heterosexual
M
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,

Mump answered 14/8, 2015 at 12:25 Comment(0)
S
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.

Saundra answered 30/9, 2015 at 10:9 Comment(1)
Nice one. While it didn't help me directly it did open my mind to another solution. Thanks a lot!Triage
L
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-->

Latinalatinate answered 20/7, 2019 at 22:35 Comment(0)
S
-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 :)

Spitzer answered 15/7, 2012 at 16:31 Comment(4)
While this works, it will only work if the array has three elements.Compare
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).Spitzer
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.Compare
In the last comment on his question he says that the elements won't have the same structure, so hardcoding each structure is inevitable.Spitzer

© 2022 - 2024 — McMap. All rights reserved.