Render dynamic components the simple way in Angular – ngComponentOutlet
Tags: angular, rxjs, tutorial, webdev
Published: 2023-04-13 00:51:08

##Introduction
In Angular, there are a few ways to render templates and components dynamically. There is ngTemplateOutlet that can render different instances of ng-template conditionally. When we use components, we can apply ngComponentOutlet to render the dynamic components the simple way or ViewContainerRef the complex way.

In this blog post, I created a new component, PokemonTabComponent, that is consisted of hyperlinks and a ng-container element. When clicking a link, the component renders PokemonStatsComponent, PokemonAbilitiesComponent or both dynamically. NgComponentOutlet helps render the dynamic components the simple way 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: [
    PokemonStatsComponent, PokemonAbilitiesComponent
  ],
  template: `
    <div style="padding: 0.5rem;">
      <ul>
        <li><a href="#" #selection data-type="ALL">All</a></li>
        <li><a href="#" #selection data-type="STATISTICS">Stats</a></li>
        <li><a href="#" #selection data-type="ABILITIES">Abilities</a></li>
      </ul>
    </div>
    <app-pokemon-stats [pokemon]="pokemon"></app-pokemon-stats>
    <app-pokemon-abilities [pokemon]="pokemon"></app-pokemon-abilities>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
  @Input()
  pokemon: FlattenPokemon;
}
```

In `PokemonTabComponent` standalone component, nothing happened when clicking the links. However, the behaviour would change when I added new codes and and `ngComponentOutlet` in the inline template to render the dynamic components.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bokthe6ufittzisylld1.png)

###Compose RxJS code to map mouse click to components
```typescript
// pokemon-tab.enum.ts

export enum POKEMON_TAB {
    ALL = 'all',
    STATISTICS = 'statistics',
    ABILITIES = 'abilities'
}
```

First, I defined enum to represent different mouse clicks.

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

componentMap = {
    [POKEMON_TAB.STATISTICS]: [PokemonStatsComponent],
    [POKEMON_TAB.ABILITIES]: [PokemonAbilitiesComponent],
    [POKEMON_TAB.ALL]: [PokemonStatsComponent, PokemonAbilitiesComponent],
}
```

Then, I defined an object map to map the enum members to the component lists.

```typescript
@ViewChildren('selection', { read: ElementRef })
selections: QueryList<ElementRef<HTMLLinkElement>>;
```

Next, I used the ViewChildren() decorator to query the hyperlinks and the building blocks are in place to construct RxJS code in `ngAfterViewInit`.

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

export class PokemonTabComponent implements AfterViewInit, OnChanges {
  ...
} 
```

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

components$!: Observable<DynamicComponentArray>;

ngAfterViewInit(): void {
    const clicked$ = this.selections.map(({ nativeElement }) => fromEvent(nativeElement, 'click')
        .pipe(
          map(() => POKEMON_TAB[(nativeElement.dataset['type'] || 'ALL') as keyof typeof POKEMON_TAB]),
          map((value) => this.componentMap[value]),
        )
    );

    // merge observables to emit enum value and look up Component types
    this.components$ = merge(...clicked$)
      .pipe(startWith(this.componentMap[POKEMON_TAB.ALL]));
}
```

- ({ nativeElement }) => fromEvent(nativeElement, 'click') - create Observable that emits value when button is clicked
- map(() => POKEMON_TAB[(nativeElement.dataset[‘type’] || ‘ALL’) as keyof typeof POKEMON_TAB]) – convert the value of type data attribute to POKEMON_TAB enum member
- map((value) => this.componentMap[value]) – use POKEMON_TAB enum member to look up component list to render dynamically
- Assign the component list to clicked$ Observable

```typescript
this.components$ = merge(...clicked$)
    .pipe(startWith(this.componentMap[POKEMON_TAB.ALL]));
```

- merge(…clicked$) – merge the Observables to emit the component list
- startWith(this.componentMap[POKEMON_TAB.ALL]) – both PokemonStatsComponent and PokemonAbilitiesComponent are rendered initially

###Apply ngComponentOutlet to PokemonTabComponent

`ngComponentOutlet` directive has 3 syntaxes and I will use the syntax that expects component and injector. It is because I require to inject Pokemon object to `PokemonStatsComponent` and `PokemonAbilitiesComponent` respectively

```typescript
<ng-container *ngComponentOutlet="componentTypeExpression;
              injector: injectorExpression;
              content: contentNodesExpression;">
</ng-container>
```

In the inline template, I replaced &lt;app-pokemon-stats&gt; and &lt;app-pokemon-abilities&gt; with &lt;ng-container&gt; as the host of the dynamic components.

```typescript
<ng-container *ngFor="let component of components$ | async">
     <ng-container *ngComponentOutlet="component; injector: myInjector"></ng-container>
</ng-container>
```

In the imports array, import `NgFor`, `AsyncPipe` and `NgComponentOutlet`.

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

In the inline template, `myInjector` has not declared and I will complete the implementation in `ngAfterViewInit` and `ngOnChange`.

Let's define a new injection token for the Pokemon object

```typescript
// pokemon.constant.ts

import { InjectionToken } from "@angular/core";
import { FlattenPokemon } from "../interfaces/pokemon.interface";

export const POKEMON_TOKEN = new InjectionToken<FlattenPokemon>('pokemon_token');
```

`createPokemonInjectorFn` is a high-order function that returns a function to create an injector to inject an arbitrary Pokemon object.

```typescript
// pokemon.injector.ts

export const createPokemonInjectorFn = () => {
  const injector = inject(Injector);

  return (pokemon: FlattenPokemon) =>
    Injector.create({
      providers: [{ provide: POKEMON_TOKEN, useValue:pokemon }],
      parent: injector
    });
}
```

In `PokemonTabComponent`, I imported `POKEMON_TOKEN` and `createPokemonInjectorFn` to assign new injector to `myInjector` in `ngAfterViewInit` and `ngOnChanges`.

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

injector = inject(Injector);
myInjector!: Injector;
createPokemonInjector = createPokemonInjectorFn();
markForCheck = inject(ChangeDetectorRef).markForCheck;

ngAfterViewInit(): void {
    this.myInjector = this.createPokemonInjector(this.pokemon);
    this.markForCheck();
    ...
}

ngOnChanges(changes: SimpleChanges): void {
    this.myInjector = this.createPokemonInjector(changes['pokemon'].currentValue);
}
```

At this point, the application would not work because `PokemonStatsComponent` and `PokemonAbilitiesComponent` had not updated to inject the Pokemon object. This would be the final step to have a working example.

###Inject Pokemon to PokemonStatsComponent and PokemonAbilitiesComponent

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

export class PokemonStatsComponent {
  pokemon = inject(POKEMON_TOKEN);
}
// pokemon-abilities.component.ts

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

The following Stackblitz repo shows the final results:

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