Render dynamic components in Angular using ViewContainerRef
Tags: angular, tutorial, webdev
Published: 2023-04-30 06:54:53

##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
```typescript
// pokemon-tab.component.ts

@Component({
  selector: 'app-pokemon-tab',
  standalone: true,
  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.

![Skeleton code of the example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jd2eedwx57keyi5srp44.png)

###Map radio button selection to component types
```typescript
// pokemon-tab.component.ts

private async getComponenTypes() {
    const { PokemonStatsComponent } = await import('../pokemon-stats/pokemon-stats.component');
    const { PokemonAbilitiesComponent } = await import('../pokemon-abilities/pokemon-abilities.component');

    if (this.selection === 'ALL') {
      return [PokemonStatsComponent, PokemonAbilitiesComponent];
    } else if (this.selection === 'STATISTICS')  {
      return [PokemonStatsComponent];
    }

    return [PokemonAbilitiesComponent];    
 }
```

First, I defined a method, `getComponenTypes`, to lazy load standalone components. Then, I examine the value of `this.selection` to return component type list.

```typescript
@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.

```typescript
// 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 componentTypes = await this.getComponenTypes();

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

```typescript
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`.

```typescript
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

![ngOnChanges](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7zd72jz4000ygwwy7brc.png)

`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:

{%embed https://stackblitz.com/edit/angular-dstj5e?file=src/pokemon/pokemon-tab/pokemon-tab.component.ts %}

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.


##Resources:

- Github Repo: https://github.com/railsstudent/ng-pokemon/tree/main/projects/pokemon-demo-11
- Stackblitz: https://stackblitz.com/edit/angular-dstj5e?file=src/pokemon/pokemon-tab/pokemon-tab.component.ts
- Youtube: https://www.youtube.com/watch?v=1Torp0Xgv0U
- PokeAPI: https://pokeapi.co/