5

I am writing an HTML table component using data that is nested, such that the output might look like this:

<table>
  <tr><td>Parent</td></tr>
  <tr><td>Child 1</td></tr>
  <tr><td>Child 2</td></tr>
  <tr><td>Grandchild 1</td></tr>
</table>

I would like to create this using a recursive component as follows:

<table>
  <data-row *ngFor="let row of rows" [row]="row"></data-row>
</table>

data-row:

<tr><td>{{row.name}}</td></tr>
<data-row *ngFor="let child of row.children" [row]="child"></data-row>

However, this adds a wrapping element around the table row which breaks the table and is invalid HTML:

<table>
  <data-row>
    <tr>...</tr>
    <data-row><tr>...</tr></data-row>
  </data-row>
</table>

Is it possible to remove this data-row wrapping element?

One Solution:

One solution is to use <tbody data-row...></tbody> which is what I'm currently doing, however this leads to nested tbody elements which is against the W3C spec

Other thoughts:

I've tried using ng-container but it doesn't seem to be possible to do <ng-container data-row...></ng-container> so that was a non starter.

I have also considered ditching the use of tables, however using an HTML table is the ONLY way to allow simple copying of the data into a spreadsheet which is a requirement.

The final option I've considered would be to flatten the data before generating the table, however, since the tree can be expanded and collapsed at will, this leads to either excessive rerendering or a very complicated model.

EDIT: Here's a Plunk with my current solution (which is against spec): http://plnkr.co/edit/LTex8GW4jfcH38D7RB4V?p=preview

Josh
  • 155
  • 1
  • 2
  • 10
  • Possible duplicate of https://stackoverflow.com/questions/34556277/angular2-table-rows-as-component – G M Jun 16 '22 at 20:33

4 Answers4

1

Just use a class or attribute as the selector for the component and apply it to a table row element.

 @Component({
 selector: [data-row],

with

  <tr data-row> </tr>

or

 @Component({ 
 selector:  .data-row,

with

 <tr class="data-row"></tr>

EDIT - i can only get it to work using content projection in the child component, and then including the td elements inside the components element in the parent. See here - https://plnkr.co/edit/CDU3Gn1Fg1sWLtrLCfxw?p=preview

If you do it this way, you could query for all the rows by using ContentChildren

 import { Component, ContentChildren, QueryList }  from '@angular/core';
 import { DataRowComponent } from './wherever';

somewhere in your component...

 @ContentChildren(DataRowComponent) rows: QueryList<DataRowComponent>;

That will be defined in ngAfterContentInit

ngAfterContentInit() { 
    console.log(this.rows);  <-- will print all the data from each component
 }

Note - you can also have components that recurse (is that a word?) themselves in their own templates. In the template of data-row component, you have any number of data-row components.

diopside
  • 2,981
  • 11
  • 23
  • The problem is you can't nest one `tr` inside another and your solution leads to this. I've updated my answer with a Plunk showing what I'm trying to achieve. – Josh Aug 25 '17 at 17:20
  • I'm not sure how the solution i mentioned above would lead to nested tr elements, unless you did what i said in my note at the bottom? I wasn't suggesting the note at the bottom for your solution though, more just giving that as a general hint. – diopside Aug 25 '17 at 17:28
  • See plunker here - https://plnkr.co/edit/CDU3Gn1Fg1sWLtrLCfxw?p=preview It seems to work if you use content projection and include the td elements inside of the component in the parent template, rather than the component template. This tells me the browser won't interpret table tags correctly unless all the required tags are in the same template. So try using ng-content – diopside Aug 25 '17 at 17:31
  • I've edited my example as you suggest. As you can see, the table no longer works!! http://plnkr.co/edit/cDWaIo5kAaveSLCERdjM?p=preview – Josh Aug 25 '17 at 17:36
  • Because you're using tr as the element for the component... but then immediately putting a tr element again in the child component.template as its first element. – diopside Aug 25 '17 at 17:38
  • but if you see my comment above your last one - i can only get it to work wth content projection. It looks like it wont interpret table tags correctly unless all the required ones are in the same template. So use content projection - then you want need to manually pass the same component to itself as an input. You can just do it all explicitly in html inside the table tags of the parent component. Will probaby be easier to maintain like that – diopside Aug 25 '17 at 17:40
  • Your edit is what I think I may have to do (although I'll flatten the data structure itself rather than it's output), but this is annoying since in Angular one a simple `replace: true` on my directive would have solved this. – Josh Aug 25 '17 at 17:42
  • Your provided plunkr seems to work if you just remove the initial elements in the child component template - http://plnkr.co/edit/kX8h7U22Memlj3a61i6A?p=preview – diopside Aug 25 '17 at 17:43
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152855/discussion-between-josh-and-diopside). – Josh Aug 25 '17 at 17:43
1

I found a solution from another stackoverflow thread, so I can't take credit, but the following solution worked for me.

Put :host { display: contents; } into the data-row component .css file.

G M
  • 311
  • 1
  • 5
  • 11
0

If you wrap row components in a ng-container you should be able to get it done

<tbody>
    <ng-container *ngFor="let row of rows; let i = index">
       <data-row  [row]="row"></data-row>
    </ng-container>
</tbody>

@Component({
  selector: 'my-app',
  template: `
  <table>
    <ng-container *ngFor="let row of table">
      <tbody data-table-row [row]="row"></tbody>
    </ng-container>
  </table>
  `,
})
export class App {
  table = [

    {
      name: 'Parent', 
      children: [
        {
          name: 'Child 1'
          children: []
        },
        {
          name: 'Child 2'
          children: [
            {
              name: 'Grandchild 1'
              children: []
            }
          ]
        }
      ]
    }
  ]
}

@Component({
  selector: 'tbody[data-table-row]',
  template: `
    <tr><td>{{row.name}}</td></tr>
    <tbody *ngFor="let child of row.children" data-table-row [row]="child"></tbody>
  `
})
export class DataTableRowComponent {
  @Input() row: any;
}
Vega
  • 27,856
  • 27
  • 95
  • 103
  • this leads to `...` which is invalid HTML – Josh Aug 25 '17 at 15:55
  • It is inside `data-row` see the plunk for an example with the `tbody` method: http://plnkr.co/edit/LTex8GW4jfcH38D7RB4V?p=preview – Josh Aug 25 '17 at 17:19
  • Problem is, it doesn't hold the table form. See here: http://plnkr.co/edit/qNqomFUr2h47YeuIOsHs?p=preview – Josh Aug 25 '17 at 17:38
  • @Josh, were you able to make it work? I had a solution with
    • and an other with table, but didn't post yet. If you are interested, I would
    – Vega Aug 29 '17 at 11:16
  • No luck yet. I would be grateful of more solutions – Josh Aug 29 '17 at 14:50
  • I am dubitative that they are good enough, but maybe you will find something helpful inside. http://plnkr.co/edit/aipXRgtJ9Jv475qzpAzV?p=preview and http://plnkr.co/edit/xNUR9dswJ3h8uw67xzDk?p=preview – Vega Aug 30 '17 at 15:47
  • I wish I could use the first one (edited slightly) but being able to copy straight into a table is a requirement and this can only be done with an HTML table. The second one allows for this but relies heavily on using fixed widths for each cell which is extremely annoying since my data is dynamic. So unfortunately no luck here either – Josh Aug 30 '17 at 16:08
0

posting another answer just to show what i was talking about ... I'll leave you alone after this, promise. Heh.

http://plnkr.co/edit/XcmEPd71m2w841oiL0CF?p=preview

This example renders everything as a flat structure, but retains the nested relationships. Each item has a reference to its parent and an array of its children.

import {Component, NgModule, VERSION, Input} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `
  <table *ngIf="tableReady">
    <tr *ngFor="let row of flatList" data-table-row [row]="row">  </tr>
  </table>
  `,
})
export class App {
  tableReady = false;
  table = [
    {
      name: 'Parent',
      age: 70,
      children: [
        {
          name: 'Child 1',
          age: 40,
          children: []
        },
        {
          name: 'Child 2',
          age: 30,
          children: [
            {
              name: 'Grandchild 1',
              age: 10,
              children: []
            }
          ]
        }
      ]
    }
  ];
  flatList = [];
  ngOnInit() {   
    let flatten = (level : any[], parent? :any ) => {
     for (let item of level){ 
        if (parent) { 
          item['parent'] = parent; 
        } 
        this.flatList.push(item);
        if (item.children) { 
          flatten(item.children, item);
        }
      }
      
    } 
    flatten(this.table);
    this.tableReady = true;
  }
}

@Component({
  selector: '[data-table-row]',
  template: `
   <td>{{row.name}}</td><td>{{row.age}}</td>
  `
})
export class DataTableRowComponent {
  @Input() row: any;
}
diopside
  • 2,981
  • 11
  • 23