Customize component using ViewContainerRef and signals in Angular
Tags: angular, tutorial, signal, webdev
Published: 2023-07-21 14:49:16

##Introduction
In this blog post, I want to describe how to perform UI customization using ViewContainerRef and Angular signals. Originally, I have a PokemonTabComponent that renders dynamic components using ViewContainerRef and the components are assigned a pokemon input. After rewrite, I refactor these components to inject Pokemon service in order to access the pokemon signal. After obtaining the signal in the component, the inline template can invoke signal function to render data subsequently.

###Create a service using “Signals in a Service”
```typescript
// pokemon.service.ts

// ...omitted import statements ...

const initialValue: DisplayPokemon = {
  id: -1,
  name: '',
  height: -1,
  weight: -1,
  backShiny: '',
  frontShiny: '',
  abilities: [],
  stats: [],
};

const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => {
  const { id, name, height, weight, sprites, abilities: a, stats: statistics } = pokemon;

  const abilities: Ability[] = a.map(({ ability, is_hidden }) => ({
    name: ability.name,
    isHidden: is_hidden
  }));

  const stats: Statistics[] = statistics.map(({ stat, effort, base_stat }) => ({
    name: stat.name,
    effort,
    baseStat: base_stat,
  }));

  return {
    id,
    name,
    height,
    weight,
    backShiny: sprites.back_shiny,
    frontShiny: sprites.front_shiny,
    abilities,
    stats,
  }
}

@Injectable({
  providedIn: 'root'
})
export class PokemonService {
  private readonly pokemonIdSub = new BehaviorSubject(1);
  private readonly httpClient = inject(HttpClient);

  private readonly pokemon$ =  this.pokemonIdSub
    .pipe(
      switchMap((id) => this.httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)),
      map((pokemon) => pokemonTransformer(pokemon))
    );
  pokemon = toSignal(this.pokemon$, { initialValue });

  personalData = computed(() => {
    const { id, name, height, weight } = this.pokemon();
    return [
      { text: 'Id: ', value: id },
      { text: 'Name: ', value: name },
      { text: 'Height: ', value: height },
      { text: 'Weight: ', value: weight },
    ];
  });

  updatePokemonId(input: PokemonDelta | number) {
    if (typeof input === 'number') {
      this.pokemonIdSub.next(input); 
    } else {
      const potentialId = this.pokemonIdSub.getValue() + input.delta;
      const newId = Math.min(input.max, Math.max(input.min, potentialId));
      this.pokemonIdSub.next(newId); 
    }
  }
}
```

First, I define `PokemonService` to create `pokemon` signal and use `computed` keyword to compute `personalData` signal based on it. When `pokemonIdSub` BehaviorSubject emits an id in `updatePokemonId` method, a HTTP request is made to retrieve the specified Pokemon from the API. However, `HttpClient` returns an Observable; therefore, I convert it to signal using `toSignal` with an initial value.

After `PokemonService` defines `pokemon` and `personalData` signals, components can inject the service to access the signals and call the signal functions within their inline templates.

###The skeleton code of Pokemon Tab component
```typescript
// pokemon-tab.component.ts

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, OnInit, ViewChild, ViewContainerRef, inject } from '@angular/core';

type SelectionType = 'ALL' | 'STATISTICS' | 'ABILITIES';

@Component({
  selector: 'app-pokemon-tab',
  standalone: true,
  template: `
    <div style="padding: 0.5rem;">
      <div>
        <div>
          <input id="all" name="type" type="radio" (click)="renderComponentsBySelection('ALL')" checked />
          <label for="all">All</label>
        </div>
        <div>
          <input id="stats" name="type" type="radio" (click)="renderComponentsBySelection('STATISTICS')" />
          <label for="stats">Stats</label>
        </div>
        <div>
          <input id="ability" name="type" type="radio" (click)="renderComponentsBySelection('ABILITIES')" />
          <label for="ability">Abilities</label>
        </div>
      </div>
    </div>
    <ng-container #vcr></ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
  @ViewChild('vcr', { static: true, read: ViewContainerRef })
  vcr!: ViewContainerRef;

  selection: SelectionType = 'ALL';
  componentRefs: ComponentRef<any>[] = [];
  
  cdr = inject(ChangeDetectorRef);

  async renderComponentsBySelection(selection: SelectionType) {}
}
```

In `PokemonTabComponent` standalone component, nothing happens when clicking the radio button. However, the behaviour will change when I add new logic to call `ViewContainerRef` API to create components and trigger change detection. Therefore, I inject `ChangeDetectorRef` and assign the reference to `cdr` for change detection purpose.


###Determine component types based on radio selection
```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 define `getComponenTypes` method that loads `PokemonStatsComponent` and `PokemonAbilitiesComponent` from component files. When selection is ALL, both components are returned. When selection is STATISTICS, the method returns `PokemonStatsComponent` only. Otherwise, the method returns `PokemonAbilitiesComponent`.

###Leverage ViewContainerRef API for customization in PokemonTabComponent
```typescript
async renderComponentsBySelection(selection: SelectionType) {
    this.selection = selection;
    await this.renderDynamicComponents();
}
```

When user clicks a radio button, inline template invokes `renderComponentsBySelection` to set the selection and delegate to `renderDynamicComponents` to call ViewContainerRef API.

```typescript
destroyComponentRefs(): void {
    // release component refs to avoid memory leak
    for (const componentRef of this.componentRefs) {
      if (componentRef) {
        componentRef.destroy();
      }
    }
}

private async renderDynamicComponents() {
    const componentTypes = await this.getComponenTypes();

    // clear dynamic components shown in the container previously    
    this.vcr.clear();
    this.destroyComponentRefs();
    for (const componentType of componentTypes) {
      const newComponentRef = this.vcr.createComponent(componentType);
      // store component refs created
      this.componentRefs.push(newComponentRef);
      // run change detection in the component and child components
      this.cdr.detectChanges();
    }
}
```

When using ViewContainerRef API to create components, it is important to call `clear` to remove all components from the container and destroy all instances of `ComponentRef` to free memory. Next, `ViewContainerRef` iterates componentTypes to create components, keep track of `ComponentRef` and trigger change detection manually to display them.

If you ask where `this.vcr` comes from, it comes from the template variable, `vcr`, in the inline template.

```html
<ng-container #vcr></ng-container>
```

```typescript
@ViewChild('vcr', { static: true, read: ViewContainerRef })
vcr!: ViewContainerRef;
```

`ViewChild` selects the template variable and `this.vcr` has a valid `ViewContainerRef` object after Angular instantiates `PokemonTabComponent` component.

The application does not work at this point because dynamic components do not receive pokemon input during component creation. It is done on purpose. Next, I will rewrite other components to inject `PokemonService` and display signal value in their inline template.

###Inject PokemonService to PokemonStatsComponent and PokemonAbilitiesComponent

```typescript
// pokemon-stats.component.ts

export class PokemonStatsComponent {
  pokemon = inject(PokemonService).pokemon;
}
```

Modify inline template to obtain statistics from pokemon signal

```typescript
<div style="padding: 0.5rem;">
   <p>Stats</p>
   <ng-container *ngTemplateOutlet="content; context: { $implicit: pokemon().stats }"></ng-container>
</div>
<ng-template #content let-stats>
   <div *ngFor="let stat of stats" class="stats-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.baseStat }}</span>
     </label>
      <label>
         <span style="font-weight: bold; color: #aaa">Effort: </span>
         <span>{{ stat.effort }}</span>
      </label>
   </div>
</ng-template>
// pokemon-abilities.component.ts
```
```typescript
export class PokemonAbilitiesComponent {
  pokemon = inject(PokemonService).pokemon;
}
```

Modify inline template to obtain abilities from pokemon signal

```typescript
<div style="padding: 0.5rem;">
    <p>Abilities</p>
    <ng-container *ngTemplateOutlet="content; context: { $implicit: pokemon().abilities }"></ng-container>
</div>
<ng-template #content let-abilities>
    <div *ngFor="let ability of abilities" class="abilities-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.isHidden ? 'Yes' : 'No' }}</span>
      </label>
    </div>
</ng-template> 
```

###Use computed signal in PokemonPersonalComponent

```typescript
// pokemon-personal.component.ts

export class PokemonStatsComponent {
  personalData = inject(PokemonService).personalData;
}
```

Modify inline template to display personal data in personalData signal

```typescript
<div class="pokemon-container" style="padding: 0.5rem;">
   <ng-container *ngTemplateOutlet="details; context: { $implicit: personalData() }"></ng-container>
</div>
<ng-template #details let-personalData>
    <label *ngFor="let data of personalData">
       <span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
       <span>{{ data.value }}</span>
    </label>
</ng-template>
```

The following Stackblitz repo shows the final results:

{%embed https://stackblitz.com/edit/angular-3bvygt?file=src%2Fpokemon%2Fpokemon-tab%2Fpokemon-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-signal/tree/main/projects/pokemon-signal-demo-7
- Stackblitz: https://stackblitz.com/edit/angular-3bvygt?file=src%2Fpokemon%2Fpokemon-tab%2Fpokemon-tab.component.ts
- PokeAPI: https://pokeapi.co/