4

I have two components. The first one represents a table of items and the second one represents one item. The first one is repeating the second one many times.

The List Component (app-list):

<table>
    <tr *ngFor="let item of items" [item]="item" app-item></tr>    
</table>

The Item Component (app-item):

<td>
    <img src="https://someimage.com/{{item.img}}.jpg">
</td>
<td>
    <h3>{{item.name}}</h3>
</td>
<td>
    {{item.description}}
</td>

In order for this to work, I had to use an attribute selector for the app-item component:

@Component({
  selector: '[app-item]'
})

This works perfectly.


Now I want to improve it and add a second row in each app-item. My problem is that the tr tag lies in the app-list component instead of the app-item component. I thought that if I move it to the app-item component, I could add another tr and be able to show two rows per one item. So this is what I did. After that I used ng-container to repeat the items in my app-list, in order to avoid adding a wrapper tag around my two rows:

<ng-container *ngFor="let item of items" [item]="item" app-item></ng-container>

This solution did not work. I got the following error:

ERROR TypeError: el.setAttribute is not a function
    at EmulatedEncapsulationDomRenderer2.push../node_modules/@angular/platform-browser/fesm5/platform-browser.js.DefaultDomRenderer2.setAttribute (platform-browser.js:1089)
    at EmulatedEncapsulationDomRenderer2.push../node_modules/@angular/platform-browser/fesm5/platform-browser.js.EmulatedEncapsulationDomRenderer2.applyToHost (platform-browser.js:1157)
    at DomRendererFactory2.push../node_modules/@angular/platform-browser/fesm5/platform-browser.js.DomRendererFactory2.createRenderer (platform-browser.js:1015)

Can you help me resolve this error or suggest another implementation?


EDIT: SOLUTION

The better version @Serhiy is suggesting

The table:

<table>
  <app-item *ngFor="let item of items" [item]="item" remove-component-tag></app-item>
</table>

The directive:

import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[remove-component-tag]'
})
export class RemoveComponentTagDirective {
  constructor(private el: ElementRef) {

    let element = el.nativeElement;
    let children = el.nativeElement.childNodes;

    setTimeout(()=>{
      let reversedChildren = [];
      children.forEach(child => {
        reversedChildren.unshift(child);
      });
      reversedChildren.forEach(child => {
        element.parentNode.insertBefore(child, element.nextSibling);
      });
      element.remove(element);
    }, 0);

  }
}

The timeout is necessary for some reason and works even with 0.

Stefanos Kargas
  • 10,547
  • 22
  • 76
  • 101

3 Answers3

4

I can't see the right "angular" way to do it, but you should be able to use directives to clear your html during render.

Saw this approach in comments here: Angular2 : render a component without its wrapping tag

I tried that and it worked for me:

Parent component:

<table>

  <div *ngFor="let item of items">
    <app-item [item]="item" remove-wrapper></app-item>  
  </div>

</table>

Child component:

<tr>
    <td>
      <img src="https://someimage.com/{{item.img}}.jpg">
    </td>

    <td>
      <h3>{{item.name}}</h3>
    </td>

    <td>
      {{item.description}}
    </td>      

</tr>

<tr>
    <td>
      <img src="https://someimage.com/{{item.img}}.jpg">
    </td>

    <td>
      <h3>{{item.name + ' 2'}}</h3>
    </td>

    <td>
      {{item.description + ' 2'}}
    </td>      

</tr>

Directive:

@Directive({
  selector: '[remove-wrapper]'
})

export class RemoveWrapperDirective {

  constructor(private el: ElementRef) {

    let parent = el.nativeElement.parentElement;
    let children = el.nativeElement.childNodes;

    setTimeout(()=>{
      parent.parentNode.insertBefore(children[1], parent.nextSibling);
      parent.parentNode.insertBefore(children[0], parent.nextSibling);
      parent.remove(parent);
    }, 10);

  }
}

Without a timeout, it crashed for me. The code can be improved, but you can start from here.

Serhiy
  • 359
  • 1
  • 7
2

thank you for that solution. I want to add a correction for your code to avoid the usage of the setTimeout function. Implementing the OnInit interface for the directive and move the code from the constructor to the ngOnInit method will keep the code clean.

import { Directive, OnInit, ElementRef } from '@angular/core';

@Directive({
  selector: '[remove-component-tag]'
})
export class RemoveComponentTagDirective implements OnInit{

  constructor(private el: ElementRef) { }

  ngOnInit() {
    let element = this.el.nativeElement;
    let children = this.el.nativeElement.childNodes;

    let reversedChildren = [];
    children.forEach(child => {
      reversedChildren.unshift(child);
    });
    reversedChildren.forEach(child => {
      element.parentNode.insertBefore(child, element.nextSibling);
    });
    element.remove(element);
  }
}

Take a look at Angular lifecycle hooks

fr43nk
  • 129
  • 5
0

Would using component syntax instead of directive syntax help here?

Instead of:

<ng-container *ngFor="let item of items" [item]="item" app-item></ng-container>

Try:

<ng-container *ngFor="let item of items">
    <app-item [item]="item"></app-item>
</ng-container> 
fralewsmi
  • 92
  • 2
  • 12
  • 1
    I have already tried this. The app-item tag is shown in the final HTML around the tr tags and this ruins the table. My goal is to get rid of all other tags except the trs – Stefanos Kargas Jun 21 '19 at 08:04