Introduction
In Angular, we use ngTemplateOutlet
to display ngTemplate when we know which ones and the exact number of them during development time. The other option is to render ngTemplates using ViewContainerRef class. ViewContainerRef
class has createEmbeddedView
method that instantiates embedded view and inserts it to a container. When there are many templates that render conditionally, ViewContainerRef solution is cleaner than multiple ngIf/ngSwitch expressions that easily clutter inline template.
In this blog post, I created a new component, PokemonTabComponent, that is consisted of radio buttons, two ngTemplates and a ng-container
element. When clicking a radio button, the component renders stats
ngTemplate, abilities
ngTemplate or both dynamically. Rendering ngTemplates using ViewContainerRef is more sophisticated than ngTemplateOutlet
and we will see its usage for the rest of the post.
The skeleton code of Pokemon Tab component
// pokemon-tab.component.ts
@Component({
selector: 'app-pokemon-tab',
standalone: true,
imports: [NgFor],
template: `
<div style="padding: 0.5rem;" class="container">
<div>
<div>
<input type="radio" id="all" name="selection" value="all" checked>
<label for="all">All</label>
</div>
<div>
<input type="radio" id="stats" name="selection" value="stats">
<label for="stats">Stats</label>
</div>
<div>
<input type="radio" id="abilities" name="selection" value="abilities">
<label for="abilities">Abilities</label>
</div>
</div>
<ng-container #vcr></ng-container>
</div>
<ng-template #stats let-pokemon>
<div>
<p>Stats</p>
<div *ngFor="let stat of pokemon.stats" class="flex-container">
<label>
<span style="font-weight: bold; color: #aaa">Name: </span>
<span>{{ stat.name }}</span>
</label>
<label>
<span style="font-weight: bold; color: #aaa">Base Stat: </span>
<span>{{ stat.base_stat }}</span>
</label>
<label>
<span style="font-weight: bold; color: #aaa">Effort: </span>
<span>{{ stat.effort }}</span>
</label>
</div>
</div>
</ng-template>
<ng-template #abilities let-pokemon>
<div>
<p>Abilities</p>
<div *ngFor="let ability of pokemon.abilities" class="flex-container">
<label>
<span style="font-weight: bold; color: #aaa">Name: </span>
<span>{{ ability.name }}</span>
</label>
<label>
<span style="font-weight: bold; color: #aaa">Is hidden? </span>
<span>{{ ability.is_hidden ? 'Yes' : 'No' }}</span>
</label>
<label> </label>
</div>
</div>
</ng-template>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
@Input()
pokemon: FlattenPokemon;
}
In PokemonTabComponent
standalone component, nothing happened when clicking the radio buttons. However, the behavior would change when I called ViewContainerRef class to create embedded views from ngTemplates and insert the templates to the container named vcr. The end result is to display templates "stats" and/or "abilities" conditionally without using structure directives such as ngIf and ngSwitch.
Access embedded ngTemplates in PokemonTabComponent
In order to create embedded views, I need to pass TemplateRef to createEmbeddedView
method of ViewContainerRef
class. I can specify the template variable of ngTemplate
in ViewChild to obtain the TemplateRef.
// pokemon-tab.component.ts
// obtain reference to ng-container element
@ViewChild('vcr', { static: true, read: ViewContainerRef })
vcr!: ViewContainerRef;
// obtain reference ngTemplate named stats
@ViewChild('stats', { static: true, read: TemplateRef })
statsRef!: TemplateRef<any>;
// obtain reference ngTemplate named abilities
@ViewChild('abilities', { static: true, read: TemplateRef })
abilitiesRef!: TemplateRef<any>;
In the above codes, statsRef
and abilitiesRef
are the TemplateRef instances of stats
and abilities
ngTemplates respectively.
Add click event handler to radio buttons
When I click any radio button, I wish to look up the TemplateRef instances, create embedded views and append them to vcr. In inline template, I add click event handler to the radio buttons that execute renderDynamicTemplates
method to render templates.
// pokemon-tab.component.ts
template:`
...
<div>
<input type="radio" id="all" name="selection" value="all"
checked (click)="selection = 'ALL'; renderDyanmicTemplates();">
<label for="all">All</label
</div>
<div>
<input type="radio" id="stats" name="selection" value="stats"
(click)="selection = 'STATISTICS'; renderDyanmicTemplates();">
<label for="stats">Stats</label>
</div>
<div>
<input type="radio" id="abilities" name="selection" value="abilities"
(click)="selection = 'ABILITIES'; renderDyanmicTemplates();">
<label for="abilities">Abilities</label>
</div>
...
`
selection: 'ALL' | 'STATISTICS' | 'ABILITIES' = 'ALL';
embeddedViewRefs: EmbeddedViewRef<any>[] = [];
cdr = inject(ChangeDetectorRef);
private getTemplateRefs() {
if (this.selection === 'ALL') {
return [this.statsRef, this.abilitiesRef];
} else if (this.selection === 'STATISTICS') {
return [this.statsRef];
}
return [this.abilitiesRef];
}
renderDyanmicTemplates(currentPokemon?: FlattenPokemon) {
const templateRefs = this.getTemplateRefs();
const pokemon = currentPokemon ? currentPokemon : this.pokemon;
this.vcr.clear();
for (const templateRef of templateRefs) {
const embeddedViewRef = this.vcr.createEmbeddedView(templateRef, { $implicit: pokemon });
this.embeddedViewRefs.push(embeddedViewRef);
// after appending each embeddedViewRef to conta iner, I trigger change detection cycle
this.cdr.detectChanges();
}
}
this.selection
keeps track of the currently selected radio button and the value determines the template/templates that get(s) rendered in the container.
const templateRefs = this.getTemplateRefs();
examines the value of this.selection
, constructs and returns TemplateRef[]. When the selection is 'ALL', getTemplateRefs
returns both template references. When the selection is 'STATISTICS', getTemplateRefs
returns the template reference of stats template in an array. Otherwise, the method returns the template reference of abilities template in an array.
this.vcr.clear();
clears all components from the container and inserts new components dynamically
const embeddedViewRef = this.vcr.createEmbeddedView(templateRef, { $implicit: pokemon });
instantiates and appends the new embedded view to the container, and returns a EmbeddedViewRef.
{ $implicit: pokemon }
is the template context and the template has a local variable named pokemon that references a Pokemon object.
this.embeddedViewRefs.push(embeddedViewRef);
stores all the EmbeddedViewRef instances and later I destroy them in ngOnDestroy
to avoid memory leak.
this.cdr.detectChanges();
triggers change detection to update the component and its child components.
This summarizes how to render ngTemplates using createEmbeddedView method of ViewContainerRef Class.
Destroy embedded views in OnDestroy lifecycle hook
Implement OnDestroy interface by providing a concrete implementation of ngOnDestroy
.
export class PokemonTabComponent implements OnDestroy {
...other logic...
ngOnDestroy() {
// destroy embeddedViewRefs to avoid memory leak
for (const viewRef of this.embeddedViewRefs) {
if (viewRef) {
viewRef.destroy();
}
}
}
}
The method iterates embeddedViewRefs
array and frees the memory of each EmbeddedViewRef to avoid memory leak.
Render embedded views in ngOnInit
When the application is initially loaded, the page is blank because it has not called renderDynamicTemplates
yet. It is easy to solve by implementing OnInit interface and calling the method in the body of ngOnInit
.
export class PokemonTabComponent implements OnDestroy, OnInit {
...
ngOnInit(): void {
this.renderDynamicTemplates();
}
}
When Angular runs ngOnInit , the initial value of this.selection
is 'ALL' and renderDynamicTemplates
displays both templates at first.
Now, the initial load renders both templates but I have another problem. Button clicks and form input change do not update the Pokemon input of the embedded views. It can be solved by implementing OnChanges interface and calling renderDynamicTemplates
again in ngOnChanges
.
Re-render embedded views in ngOnChanges
export class PokemonTabComponent implements OnDestroy, OnInit, OnChanges {
...
ngOnChanges(changes: SimpleChanges): void {
this.renderDynamicTemplates(changes['pokemon'].currentValue);
}
}
changes['pokemon'].currentValue
is the new Pokemon input. this.renderDynamicTemplates(changes['pokemon'].currentValue)
passes the new Pokemon to template context and the new embedded views display the new value in the container.
The following Stackblitz repo shows the final results:
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.
Top comments (0)