43

I'm looking for a way to add rows to a table. My data structure looks like that:

  rows = [
    { name : 'row1', subrows : [{ name : 'row1.1' }, { name : 'row1.2' }] },
    { name : 'row2' }
  ];

I want to create a table which looks like that:

  table
     row1
     row1.1
     row1.2
     row2

Is that possible with angular js ng-repeat? If not, what would be a "angular" way of doing that?

Edit: Flatten the array would be a bad solution because if i can iterate over the sub elements i could use different html tags inside the cells, other css classes, etc.

schlingel
  • 8,560
  • 7
  • 34
  • 62
  • Convert the data structure into a flat array first, then use that new array to construct that table as a normal loop (like the current answer, without the sub-loop) – Ian Mar 12 '13 at 14:06
  • For example, this ( http://jsfiddle.net/t6RLz/ ) turns the array into a new array of 4 items (the original 2 and the 2 subrows, in correct order). Then, just loop like the answer below (since I know nothing about angularjs) without the inner loop. I'm sure the code in the jsFiddle could be enhanced and/or shortened, but it's more or less just to give you the idea – Ian Mar 12 '13 at 14:14
  • 1
    Then add a new property specifically to the subrow objects, to indicate they are a subrow. Then when looping in the HTML, check for the existence of that specific property, and do stuff based on it. – Ian Mar 12 '13 at 14:32

6 Answers6

75

More than one year later but found a workaround, at least for two levels (fathers->sons).
Just repeat tbody's:

<table>
  <tbody ng-repeat="row in rows">
    <tr>
      <th>{{row.name}}</th>
    </tr>
    <tr ng-repeat="sub in row.subrows">
      <td>{{sub.name}}</td>
    </tr>
  </tbody>
</table>

As far as I know all browsers support multiple tbody elements inside a table.

Ivan Ferrer Villa
  • 2,129
  • 1
  • 26
  • 23
28

More than 3 years later, I have been facing the same issue, and before writing down a directive I tried this out, and it worked well for me :

<table>
    <tbody>
        <tr ng-repeat-start="row in rows">
            <td>
                {{ row.name }}
            </td>
        </tr>
        <tr ng-repeat-end ng-repeat="subrow in row.subrows">
            <td>
                {{ subrow.name }}
            </td>
        </tr>
    </tbody>
</table>
  • This should indeed be the accepted answer. Is it possible to nest another `ng-repeat-start` inside an `ng-repeat-start`? – Thorkil Holm-Jacobsen Feb 16 '17 at 11:03
  • I had no idea about this! I was able to go 3 levels deep in the table hierarchy with nested `ng-repeat-start`s. Definitely should be the answer – brettvd Feb 16 '17 at 21:43
  • 2
    Yes @ThorkilHolm-Jacobsen. It solved my hierarchy table problem very nicely. Hint: If you need multiple nested together, utilize hidden `tr`s to contain the `ng-repeat-end` – brettvd Feb 16 '17 at 21:44
  • Simplest but killer solution! Also you can access properties of parent `row` object. – Farhan Ghumra Mar 01 '18 at 07:12
  • It is indeed possible to have several nested ng-repeat-start, you just have to have the equivalent in number of ng-repeat-end for AngularJS not to spit out errors. – Edgar Quintero Sep 18 '18 at 01:26
23

You won't be able to do this with ng-repeat. You can do it with a directive, however.

<my-table rows='rows'></my-table>

Fiddle.

myApp.directive('myTable', function () {
    return {
        restrict: 'E',
        link: function (scope, element, attrs) {
            var html = '<table>';
            angular.forEach(scope[attrs.rows], function (row, index) {
                html += '<tr><td>' + row.name + '</td></tr>';
                if ('subrows' in row) {
                    angular.forEach(row.subrows, function (subrow, index) {
                        html += '<tr><td>' + subrow.name + '</td></tr>';
                    });
                }
            });
            html += '</table>';
            element.replaceWith(html)
        }
    }
});
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • That's a really good answer. I think it would be even more descriptive to pack the expanding of the rows into a directive which is added to the tr element. – schlingel Mar 12 '13 at 16:04
  • @schlingel, that doesn't seem to be possible: [fiddle](http://jsfiddle.net/mrajcok/vGUsu/4/). I don't think we can use the link function to put another `...` element inside an existing one (i.e., one that is using ng-repeat). – Mark Rajcok Mar 12 '13 at 17:42
  • you're right, after() doesn't seem to work in this context as expected. – schlingel Mar 12 '13 at 17:45
  • Lovely answer...seems to reiterate that Angular can't do everything! Thanks @MarkRajcok – AdityaSaxena Mar 22 '14 at 18:02
  • Great answer, I've made a small improvement so you can have as much levels as you would like, not just 2: http://jsfiddle.net/vGUsu/108/ . Needed this for my own project :) – Erik van de Ven Mar 02 '16 at 23:01
15

I'm a bit surprised that so many are advocating custom directives and creating proxy variables being updated by $watch.

Problems like this are the reason that AngularJS filters were made!

From the docs:

A filter formats the value of an expression for display to the user.

We aren't looking to manipulate the data, just format it for display in a different way. So let's make a filter that takes in our rows array, flattens it, and returns the flattened rows.

.filter('flattenRows', function(){
return function(rows) {
    var flatten = [];
    angular.forEach(rows, function(row){
      subrows = row.subrows;
      flatten.push(row);
      if(subrows){
        angular.forEach(subrows, function(subrow){
          flatten.push( angular.extend(subrow, {subrow: true}) );
        });
      }
    });
    return flatten;
}
})

Now all we need is to add the filter to ngRepeat:

<table class="table table-striped table-hover table-bordered">
  <thead>
    <tr>
      <th>Rows with filter</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="row in rows | flattenRows">
          <td>{{row.name}}</td>
      </tr>
  </tbody>
</table>

You are now free to combine your table with other filters if desired, like a search.

While the multiple tbody approach is handy, and valid, it will mess up any css that relies on the order or index of child rows, such as a "striped" table and also makes the assumption that you haven't styled your tbody in a way that you don't want repeated.

Here's a plunk: http://embed.plnkr.co/otjeQv7z0rifPusneJ0F/preview

Edit:I added a subrow value and used it in the table to show which rows are subrows, as you indicated a concern for being able to do that.

Colton McCormack
  • 771
  • 11
  • 23
6

Yes, it's possible:

Controller:

app.controller('AppController',
    [
      '$scope',
      function($scope) {
        $scope.rows = [
          { name : 'row1', subrows : [{ name : 'row1.1' }, { name : 'row1.2' }] },
          { name : 'row2' }
        ];

      }
    ]
  );

HTML:

<table>
  <tr ng-repeat="row in rows">
    <td>
      {{row.name}}
      <table ng-show="row.subrows">
        <tr ng-repeat="subrow in row.subrows">
          <td>{{subrow.name}}</td>
        </tr>
      </table>
    </td>
  </tr>
</table>

Plunker

In case you don't want sub-tables, flatten the rows (while annotating subrows, to be able to differentiate):

Controller:

function($scope) {
  $scope.rows = [
    { name : 'row1', subrows : [{ name : 'row1.1' }, { name : 'row1.2' }] },
    { name : 'row2' }
  ];

  $scope.flatten = [];
  var subrows;
  $scope.$watch('rows', function(rows){
    var flatten = [];
    angular.forEach(rows, function(row){
      subrows = row.subrows;
      delete row.subrows;
      flatten.push(row);
      if(subrows){
        angular.forEach(subrows, function(subrow){
          flatten.push( angular.extend(subrow, {subrow: true}) );
        });
      }
    });
    $scope.flatten = flatten;
  });

}

HTML:

<table>
  <tr ng-repeat="row in flatten">
    <td>
      {{row.name}}
    </td>
  </tr>
</table>

Plunker

Stewie
  • 60,366
  • 20
  • 146
  • 113
  • 1
    That's the base case. I don't want a table in a table, I want the subrows in the same table underneath each other. – schlingel Mar 12 '13 at 13:56
  • In that case you should simply flatten your `rows` object so that all row definitions are under same node. There's now way to do that using ng-repeat alone. – Stewie Mar 12 '13 at 14:15
0

Here is an example. This code prints all names of all the people within the peopeByCity array.

TS:

export class AppComponent {
  peopleByCity = [
    {
      city: 'Miami',
      people: [
        {
          name: 'John', age: 12
        }, {
          name: 'Angel', age: 22
        }
      ]
    }, {
      city: 'Sao Paulo',
      people: [
        {
          name: 'Anderson', age: 35
        }, {
          name: 'Felipe', age: 36
        }
      ]
    }
  ]
}

HTML:

<div *ngFor="let personsByCity of peopleByCity">
  <div *ngFor="let person of personsByCity.people">
    {{ person.name }}
  </div>
</div>
codepleb
  • 10,086
  • 14
  • 69
  • 111