How to use ng-repeat without an html element

Angularjs

Angularjs Problem Overview


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>          

Angularjs Solutions


Solution 1 - Angularjs

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.

Solution 2 - Angularjs

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>

Solution 3 - Angularjs

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.

Solution 4 - Angularjs

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>

Solution 5 - Angularjs

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,

Solution 6 - Angularjs

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.

Solution 7 - Angularjs

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-->

Solution 8 - Angularjs

<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 :)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionfadedbeeView Question on Stackoverflow
Solution 1 - AngularjsjmagnussonView Answer on Stackoverflow
Solution 2 - AngularjsMark RajcokView Answer on Stackoverflow
Solution 3 - AngularjsDuka ÁrpádView Answer on Stackoverflow
Solution 4 - AngularjsbtfordView Answer on Stackoverflow
Solution 5 - AngularjsagelbessView Answer on Stackoverflow
Solution 6 - AngularjsAr EsView Answer on Stackoverflow
Solution 7 - AngularjsClintView Answer on Stackoverflow
Solution 8 - AngularjsRenan Tomal FernandesView Answer on Stackoverflow