Angular 2 Use a "Template" for Ng-Content to Use Inside Component Loop

Angular 2 use a template for ng-content to use inside component loop

Solved this with the following:

Component template usage:

<search-field>
<ng-template let-item>
<span><strong>{{item.username}}</strong></span>
<span>{{item.email}}</span>
</ng-template>
</search-field>

Component template definition:

<div class="search-container">
<div class="search-input">
<input type="text" class="form-control" placeholder="Search users..." [(ngModel)]="searchString" (ngModelChange)="searchStringChanged($event)">
<div class="md-icon">search</div>
</div>

<ul class="search-results" *ngIf="searchResults.length > 0">
<li class="search-results__item" *ngFor="let item of searchResults">
<ng-template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{$implicit: item}"></ng-template>
</li>
</ul>
</div>

Component class:

@Component({...})
export class SearchFieldComponent {
@ContentChild(TemplateRef) templateRef: TemplateRef<any>;

// ...
}

The explanation:

Using ng-template, I can use the let-item syntax, where item is the data that will be passed into the template on each iteration of the loop.

And in order to make the above possible, in the search-field component I use ng-template with ngTemplateOutlet as the template reference, and ngTemplateOutletContext is given the value {$implicit: item}, where item is the data I want to pass into the template.

Lastly, in the component class I need to use ContentChild to get the reference to the template to use in the ngTemplateOutlet.

ng-content in for loop

You can take a look at how angular material implemented tabs control.

There are some caveats about this approach https://github.com/angular/angular/issues/18691 but anyway here is simplified version:

tab.component.ts

@Component({
selector: 'app-tab',
template: `
<ng-template>
<ng-content></ng-content>
</ng-template>`
})
export class TabComponent {
...

@ViewChild(TemplateRef) template: TemplateRef<any>;
}

tabs.component.ts

<div *ngFor="let tab of tabs; let i = index" 
class="tab-pane" [ngClass]="{'active': i === 0}"...>
<ng-container *ngTemplateOutlet="tab.template"></ng-container>
</div>

Plunker Example

How to loop through section elements to project with ng-content

I would create a directive like:

@Directive({
selector: '[stepper-section]'
})
export class StepperSectionDirective {}

then add stepper-section attribute to each of sections:

<stepper>
<section stepper-section>content here<section>
<section stepper-section>content here<section>
<section stepper-section>content here<section>
</stepper>

and finally make use of @ContentChildren decorator to query all the sections:

@ContentChildren(StepperSectionDirective) sections: QueryList<StepperSectionDirective>;

Ng-run Example

If you want to loop through content and render it dynamically you can wrap your children with ng-template and use ngTemplateOutlet directive to render them in StepperComponent:

html

<app-stepper>
<ng-template stepper-section>Content 1</ng-template>
<ng-template stepper-section>Content 2</ng-template>
<ng-template stepper-section>Content 3</ng-template>
</app-stepper>

stepper-section.directive.ts

@Directive({
selector: '[stepper-section]'
})
export class StepperSectionDirective {
hidden = false;

constructor(public templateRef: TemplateRef<any>) {}
}

stepper.component.ts

@ContentChildren(StepperSectionDirective) sectionDirs: QueryList<StepperSectionDirective>;

stepper.component.html

<button *ngFor="let section of sections; index as i;"
[class.enabled]="activeSection === i" (click)="goTo(i)">
Step {{ i + 1 }}
</button>
<div class="content">
<ng-container *ngFor="let section of sections">
<ng-template *ngIf="!section.hidden"
[ngTemplateOutlet]="section.templateRef"></ng-template>
</ng-container>
</div>

Ng-run Example

The difference between these two approaches is that in the first case all child content gets rendered and we only manipulate display: none to those steps we want to hide.

In the second approach we have more control on what we want to render and we render only one step at specific time.

How to render multiple ng-content inside an ngFor loop using Angular 4?

The best option in your case would be trancluding ng-template by using ngTemplateOutlet like:

<app-table [data]="myData">
<ng-template #lineHeader let-line>
<div>
{{line.name}}
</div>
</ng-template>
<ng-template #lineContent let-element>
<div>
{{element.name}}
</div>
</ng-template>
</app-table>

app-table.component.ts

@Component({
selector: 'app-table',
template: `
<div *ngFor="let line of data">
<ng-container *ngTemplateOutlet="lineHeaderTmpl,
context: { $implicit: line }"></ng-container>
<div *ngFor="let element of line.values">
<ng-container *ngTemplateOutlet="lineContentTmpl,
context: { $implicit: element }"></ng-container>
</div>
</div>
`
})
export class AppTableComponent {
@Input() data: any;

@ContentChild('lineHeader') lineHeaderTmpl: TemplateRef<any>;
@ContentChild('lineContent') lineContentTmpl: TemplateRef<any>;
}

Stackblitz Example

Iterate through ng-content in template

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>

Can an ng-content be used inside of an ngFor?

It is possible but probably doesn't produce the desired result. Children passed by the parent can only projected once (no matter how many <ng-content> are there. If the <ng-content> elements don't select specific and different parts of the passed children using the select attribute, then everything is projected to the first <ng-content> with the default selector (none).

To make this work you probably want a custom ngFor that repeats the content passed to <ng-content>.

NgFors ngForTemplate might help in your use case.

See also Angular2 child component as data



Related Topics



Leave a reply



Submit