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.

3
  • 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) Commented Mar 12, 2013 at 14:06
  • For example, this ( 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 Commented Mar 12, 2013 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. Commented Mar 12, 2013 at 14:32

6 Answers 6

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.

Sign up to request clarification or add additional context in comments.

7 Comments

Good to know. I love how flexible all this is
If you use bootstrap table-striped, this may not display how you want
@josh it should be to the css to adapt to semantic html, not the other way around. But here angular and bootstrap are equally good at abusing the dom...
It's awesome, it saved me a lot of unnecessary code
|
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>

5 Comments

This should indeed be the accepted answer. Is it possible to nest another ng-repeat-start inside an ng-repeat-start?
I had no idea about this! I was able to go 3 levels deep in the table hierarchy with nested ng-repeat-starts. Definitely should be the answer
Yes @ThorkilHolm-Jacobsen. It solved my hierarchy table problem very nicely. Hint: If you need multiple nested together, utilize hidden trs to contain the ng-repeat-end
Simplest but killer solution! Also you can access properties of parent row object.
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.
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)
        }
    }
});

5 Comments

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, that doesn't seem to be possible: fiddle. I don't think we can use the link function to put another <tr>...</tr> element inside an existing one (i.e., one that is using ng-repeat).
you're right, after() doesn't seem to work in this context as expected.
Lovely answer...seems to reiterate that Angular can't do everything! Thanks @MarkRajcok
Great answer, I've made a small improvement so you can have as much levels as you would like, not just 2: jsfiddle.net/vGUsu/108 . Needed this for my own project :)
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.

1 Comment

Great answer! I like the idea. :)
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

2 Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.