Introduction
In Angular, ngComponentOutlet
is the simple way to render components dynamically. The other option is to render dynamic components using ViewContainerRef
class. ViewContainerRef
class has createComponent
method that instantiates component and inserts it to a container. It is a powerful technique to add components at runtime; they are not loaded in main bundle to keep the bundle size small. When there are many components that render conditionally, ViewContainerRef solution is cleaner than multiple ng-if expressions that easily bloat the inline template.
In this blog post, I created a new component, PokemonTabComponent, that is consisted of radio buttons and a ng-container
element. When clicking a radio button, the component renders PokemonStatsComponent, PokemonAbilitiesComponent or both dynamically. Rendering dynamic components using ViewContainerRef is more sophisticated than ngComponentOutlet
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: [PokemonStatsComponent, PokemonAbilitiesComponent],
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>
`,
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 started to add new codes to render dynamic components using ViewContainerRef. The end result is to display the components in the ng-container
named vcr.
Map radio button selection to component types
// pokemon-tab.enum.ts
export enum POKEMON_TAB {
ALL = 'all',
STATISTICS = 'statistics',
ABILITIES = 'abilities'
}
First, I defined enum to represent different radio button selections.
// pokemon-tab.component.ts
componentTypeMap = {
[POKEMON_TAB.ALL]: [PokemonStatsComponent, PokemonAbilitiesComponent],
[POKEMON_TAB.STATISTICS]: [PokemonStatsComponent],
[POKEMON_TAB.ABILITIES]: [PokemonAbilitiesComponent],
};
Then, I defined an object map to map the enum members to the component type lists.
@ViewChild('vcr', { static: true, read: ViewContainerRef })
vcr!: ViewContainerRef;
Next, I used the ViewChild()
decorator to obtain the reference to ng-container
element and invoke ViewContainerRef
class to append dynamic components to the container.
Add click event handler to radio buttons
When I click any radio button, I wish to look up the component types in componentTypeMap
, iterate the list to create new component references and append them to vcr
. In inline template, I add click event handler to the radio buttons that execute renderDynamicComponents
method to render dynamic components.
// pokemon-tab.component.ts
template:`
...<div>
<input type="radio" id="all" name="selection" value="all"
checked (click)="selection = 'ALL'; renderDynamicComponents();">
<label for="all">All</label>
</div>
<div>
<input type="radio" id="stats" name="selection" value="stats"
(click)="selection = 'STATISTICS'; renderDynamicComponents();">
<label for="stats">Stats</label>
</div>
<div>
<input type="radio" id="abilities" name="selection" value="abilities"
(click)="selection = 'ABILITIES'; renderDynamicComponents();">
<label for="abilities">Abilities</label>
</div>
...
`
selection: 'ALL' | 'STATISTICS' | 'ABILITIES' = 'ALL';
componentRefs: ComponentRef<PokemonStatsComponent | PokemonAbilitiesComponent>[] = [];
cdr = inject(ChangeDetectorRef);
async renderDynamicComponents(currentPokemon?: FlattenPokemon) {
const enumValue = POKEMON_TAB[this.selection as keyof typeof POKEMON_TAB];
const componentTypes = this.componentTypeMap[enumValue];
// clear dynamic components shown in the container previously
this.vcr.clear();
for (const componentType of componentTypes) {
const newComponentRef = this.vcr.createComponent(componentType);
newComponentRef.instance.pokemon = currentPokemon ? currentPokemon : this.pokemon;
// store component refs created
this.componentRefs.push(newComponentRef);
// run change detection in the component and child components
this.cdr.detectChanges();
}
}
this.selection
keeps track of the currently selected radio button and the value determines the component/components that get(s) rendered in the container.
this.vcr.clear();
removes all components from the container and inserts new components dynamically
const newComponentRef = this.vcr.createComponent(componentType);
instantiates and appends the new component to the container, and returns a ComponentRef.
newComponentRef.instance.pokemon = currentPokemon ? currentPokemon : this.pokemon;
PokemonStatsComponent and PokemonAbilitiesComponent expect a pokemon input; therefore, I assign a Pokemon object to newComponentRef.instance.pokemon
where newComponentRef.instance
is a component instance.
this.componentRefs.push(newComponentRef);
stores all the ComponentRef 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.
Destroy dynamic components in OnDestroy lifecycle hook
Implement OnDestroy interface by providing a concrete implementation of ngOnDestroy
.
export class PokemonTabComponent implements OnDestroy {
...other logic...
ngOnDestroy(): void {
// release component refs to avoid memory leak
for (const componentRef of this.componentRefs) {
if (componentRef) {
componentRef.destroy();
}
}
}
}
The method iterates componentRefs
array and frees the memory of each ComponentRef to avoid memory leak.
Render dynamic components in ngOnInit
When the application is initially loaded, the page is blank because it has not called renderDynamicComponents
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.renderDynamicComponents();
}
}
When Angular runs ngOnInit , the initial value of this.selection
is ‘ALL’ and renderDynamicComponents
displays both components at first.
Now, the initial load renders both components but I have another problem. Button clicks and form input change do not update the Pokemon input of the dynamic components. It can be solved by implementing OnChanges interface and calling renderDynamicComponents
again in ngOnChanges
.
Re-render dynamic components in ngOnChanges
export class PokemonTabComponent implements OnDestroy, OnInit, OnChanges {
...
ngOnChanges(changes: SimpleChanges): void {
this.renderDynamicComponents(changes['pokemon'].currentValue);
}
}
changes['pokemon'].currentValue
is the new Pokemon input. this.renderDynamicComponents(changes['pokemon'].currentValue)
passes the new Pokemon to assign to the new components and to display them in the container dynamically.
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.