Iterate through ng-content in template

Viewed 510

I'm using Angular 8.1.3 . This initial explanation is probably poor, but I hope the example will make it clear: I'm trying to eliminate repetition in my code, by creating a new angular component. I want this component to place different types of content that is passed into it in different places, based on the type of content. I know I can do this with <ng-content> together with the select attribute. But, this following requirement is the cause of the question: I also want to wrap each content selector match with specific html.

For example, I want to turn this:

<table class="visualization-control">
  <tr>
    <td>
    <app-input
     unimportantAttributes1
     ></app-input>
    </td>
    <td>
    <app-input unimportantAttributes2></app-input>
    </td>
  </tr>
  <tr>
    <td>
    <app-button unimportantAttributes3</app-button >
    </td>
    <td>
    <app-button unimportantAttributes4></app-button>
    </td>
    <td>
    <app-button unimportantAttributes4</app-button>
    </td>
  </tr>
</table>

into this:

<app-visualization-control>
    <app-input
     unimportantAttributes1
     ></app-input>
    <app-input unimportantAttributes2></app-input>
    <app-button unimportantAttributes3</app-button >
    <app-button unimportantAttributes4></app-button>
    <app-button unimportantAttributes5</app-button>
<app-visualization-control>

Notice that there are two rows. The the first row has two data cells in it, each containing an input, and the second three data cells, each with a button in it. But on different occurrences of this structure, I need a different number of inputs and different number of buttons. Note that the components that are seen by app-visualization-control's parents should be the same instances that are seen by app-visualization-control itself, because of data binding. That is, I should not be instancing new components, e.g. with something like *ngComponentOutlet (to my understanding this directive cannot be used with passed-in component instances, it instances and destroys it's "own" components).

Below is my best attempt, but it didn't work: the content was not rendered, though no compilation or runtime errors were thrown upon visiting the page. Further, the correct number of <td> were rendered, but they were empty.

@Component({
  selector: 'app-visualization-control',
  template: `
    <table>
      <tr>
        <td *ngFor="let inputTemplate of inputTemplates">
          <ng-container *ngTemplateOutlet="inputTemplate"></ng-container>
        </td>
      </tr>
      <tr>
        <td *ngFor="let buttonTemplate of buttonTemplates">
          <ng-container *ngTemplateOutlet="buttonTemplate"> </ng-container>
        </td>
      </tr>
    </table>
  `,
  styleUrls: ['./visualization-control.component.scss']
})
export class VisualizationControlComponent {
  @ContentChildren(InputComponent, { read: TemplateRef }) inputTemplates!: QueryList<TemplateRef<{}>>;
  @ContentChildren(ButtonComponent, { read: TemplateRef }) buttonTemplates!: QueryList<TemplateRef<{}>>;
}

PS: the example has been greatly simplified. In fact, I'm not even using <table>, I'm using ionic framework's grid system, but these details don't matter.

1 Answers

You need wrap the ButtonComponent/InputComponent content in ng-template then render the ng-template in VisualizationControlComponent. Because the ngTemplateOutlet cannot render component.

https://stackblitz.com/edit/angular-sjv3p4

ButtonComponent


import { Component, Input, Output, EventEmitter, ViewChild, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-button',
  template: `
  <ng-template>
   <button [style.color]="color" (click)="click.emit($event)">
     <ng-content></ng-content>
   </button>
  </ng-template>
  `,
})
export class ButtonComponent  {
    @Input() color: string | null = null;
    @Output() click = new EventEmitter<MouseEvent>()
    @ViewChild(TemplateRef, { static: true }) templateRef: TemplateRef<void>;
}

VisualizationControlComponent

import { Component, Input, ContentChildren, QueryList } from '@angular/core';
import { ButtonComponent } from './button.component';

@Component({
  selector: 'app-visualization-control',
  template: `
  <table>
    <tr>
      <td *ngFor="let buttonComponent of buttonComponents">
       <ng-container *ngTemplateOutlet="buttonComponent.templateRef"> </ng-container>
      </td>
    </tr>
</table>
  `,
})
export class VisualizationControlComponent  {
    @ContentChildren(ButtonComponent, { descendants: true }) buttonComponents: QueryList<ButtonComponent>;

}

Use

<app-visualization-control>
  <app-button [color]="'#e00'" (click)="onClick($event)">
    Button1
  </app-button>
  <app-button (click)="onClick($event)">
    Button2
  </app-button>
  <app-button>
    Button3
  </app-button>
</app-visualization-control>
Related