Josh
Josh

Reputation: 155

Angular 2: Remove the wrapping DOM element in a component

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

Upvotes: 5

Views: 3200

Answers (4)

G M
G M

Reputation: 361

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.

Upvotes: 1

diopside
diopside

Reputation: 3062

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

Upvotes: 0

diopside
diopside

Reputation: 3062

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.

Upvotes: 1

Vega
Vega

Reputation: 28708

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

Upvotes: 0

Related Questions