Render ngTemplates dynamically using ViewContainerRef in Angular
Tags: angular, webdev, tutorial
Published: 2023-05-01 08:31:39

##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

```typescript
// 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>&nbsp;</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.

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

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

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

```typescript
// 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<any>[].  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`.

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

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

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

{%embed https://stackblitz.com/edit/angular-rorch6?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-12
- Stackblitz: https://stackblitz.com/edit/angular-rorch6?file=src/pokemon/pokemon-tab/pokemon-tab.component.ts
- Youtube: https://www.youtube.com/watch?v=1HToLFXGcNI
- PokeAPI: https://pokeapi.co/