Search code examples
htmlangularcomponents

Create repeatable Angular component with two rows for tables


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.


Solution

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