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>
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.
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>
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>
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.
<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 :)
you may use the underscore flatten function $scope.myData= _.flatten($scope.myData);
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,
There are a couple of very simple answers to this problem that don't involve any changes to the data.
The first and probably best answer is to stop using a table and instead use nested divs in combination with the ng-switch directive. The switch is used to choose the correct HTML to output for each of the output types found in the data.
Another answer is to insert entire tables for each item. This means you keep the outer table structure you are currently using but limit it to only have one column in each row. Rather than add new rows into this single table for each item, you add an entire table for each item instead.
It will look something like the following, with the ng-repeat being on the nested tables row...
<table>
<tr>
<td>
<!-- Single row item -->
<table>
<tr ng-repeat="here">
<td>cell one</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<!-- two row item -->
<table>
<tr>
<td>cell one</td>
</tr>
<tr>
<td>cell one</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<!-- three row item -->
<table>
<tr>
<td>cell one</td>
</tr>
<tr>
<td>cell one</td>
</tr>
<tr>
<td>cell one</td>
</tr>
</table>
</td>
</tr>
</table>