Customize component using ngComponentOutlet and signals in Angular
Tags: angular, signals, tutorial, webdev
Published: 2023-07-01 13:48:35

##Introduction

In this blog post, I want to describe how to do UI customization using ngComponentOutlet and Angular signals.   Originally,  I had a PokemonTabComponent that renders dynamic components using ngComponentOutlet and RxJS.  I refactored the component to use signals and the code is surprisingly short, easy to understand and maintain.  Moreover, I apply "signals in a service" pattern to organize signals in a service, dynamic components can inject the service to access the signals and invoke signal functions in inline template to render content. 

###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 { NgComponentOutlet, NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PokemonAbilitiesComponent } from '../pokemon-abilities/pokemon-abilities.component';
import { PokemonStatsComponent } from '../pokemon-stats/pokemon-stats.component';

@Component({
  selector: 'app-pokemon-tab',
  standalone: true,
  imports: [
    PokemonAbilitiesComponent,
    PokemonStatsComponent,
    NgFor,
  ],
  template: `
    <div style="padding: 0.5rem;">
      <div>
        <div>
          <input id="all" name="type" type="radio" (click)="selectComponents('all')" checked />
          <label for="all">All</label>
        </div>
        <div>
          <input id="stats" name="type" type="radio" (click)="selectComponents('statistics')" />
          <label for="stats">Stats</label>
        </div>
        <div>
          <input id="ability" name="type" type="radio" (click)="selectComponents('abilities')" />
          <label for="ability">Abilities</label>
        </div>
      </div>
    </div>
    <ng-container *ngFor="let component of dynamicComponents">
      <ng-container></ng-container>
    </ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
  dynamicComponents = [];

  selectComponents(type: string) {}
}
```

In `PokemonTabComponent` standalone component,  nothing happened when clicking the radio button.  However, the behaviour would change when I add new logic and ngComponentOutlet to the inline template to render the dynamic components.

###Determine component types based on radio selection 

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

componentMap: Record<string, any> = {
    'statistics': [PokemonStatsComponent],
    'abilities': [PokemonAbilitiesComponent],
    'all': [PokemonStatsComponent, PokemonAbilitiesComponent],
}
```

First, I define an object map to map type to the component lists.  

```typescript
dynamicComponents = this.componentMap['all'];

selectComponents(type: string) {
    const components = this.componentMap[type];
    if (components !== this.dynamicComponents) {
      this.dynamicComponents = components;
    }
}
```

By default, dynamicComponents should render all component types, `PokemonStatsComponent` and `PokemonAbilitiesComponent`. `selectComponents` accepts a type argument, look up component list in `this.componentMap` and assigns the list back to `this.dynamicComponents`.

```html
<ng-container *ngFor="let component of dynamicComponents">
    <ng-container></ng-container>
</ng-container>
```

Next, `ngFor` iterates dynamicComponents and feeds componentType to `ngComponentOutlet` to render the component dynamically.

###Apply ngComponentOutlet to PokemonTabComponent

`ngComponentOutlet` directive has 3 syntaxes and I use the syntax that expects component type this time. It is because I inject `PokemonService` in `PokemonStatsComponent` and `PokemonAbilitiesComponent` to obtain `pokemon` signal respectively.

```html
<ng-container *ngComponentOutlet="componentTypeExpression;"></ng-container>
```

In the inline template, I replaced `<ng-container></ng-container>` with

```html
<ng-container *ngFor="let componentType of dynamicComponents">
   <ng-container *ngComponentOutlet="componentType"></ng-container>
</ng-container>
```

In the imports array,  import NgComponentOutlet.

```typescript
imports: [PokemonStatsComponent, PokemonAbilitiesComponent, NgFor, NgComponentOutlet],
```

At this point, the application does not work because `PokemonStatsComponent` and `PokemonAbilitiesComponent` have not updated to inject `PokemonService` to access `pokemon` signal. This is the final step to have a working example. 

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

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

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

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

Modify inline template to obtain abilities from pokemon signal

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

The following Stackblitz repo shows the final results:

{% embed https://stackblitz.com/edit/angular-zbeeha?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-6
- Stackblitz: https://stackblitz.com/edit/angular-zbeeha?file=src%2Fpokemon%2Fpokemon-tab%2Fpokemon-tab.component.ts
- PokeAPI: https://pokeapi.co/