[Angular]複数のcomponentの表示の切り替えをDynamic Component Loaderでシンプルにする

# はじめに

フロントエンドの機能を実装していくと、ある変数の条件によってページの内容を切り替えたい、ということはよくある。

たとえば以下のHTMLでは、セレクトボックスで指定した`selectedLanguage`の値に応じて、`switch`文で画面上に表示する文言を切り替えている。

```html:src/app/app.component.html
<div>
  <h1>Programming Language</h1>

  <!-- 表示するページをセレクトボックスから選択する -->
  <select [(ngModel)]="selectedLanguage">
    <option [value]="programmingLanguageEnum.JAVA">Java</option>
    <option [value]="programmingLanguageEnum.JAVASCRIPT">JavaScript</option>
    <option [value]="programmingLanguageEnum.TYPESCRIPT">TypeScript</option>
  </select>

  <!-- セレクトボックスで選択した内容で表示するページを切り替える -->
  <div>
    @switch (selectedLanguage) {
      @case (programmingLanguageEnum.JAVA) {
        <app-java-page
            [input]="Javaのページ"
        ></app-java-page>
      }
      @case (programmingLanguageEnum.JAVASCRIPT) {
          <app-javascript-page
            [input]="JavaScriptのページ"
          ></app-javascript-page>
      }
      @default {
        <app-typescript-page
            [input]="TypeScriptのページ"
        ></app-typescript-page>
      }
    }
  </div>
</div>
```

プルダウンでプログラミング言語を選択すると、そのプログライング言語の説明文が表示されるシンプルなページである。

![スクリーンショット 2024-02-18 18.07.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/e683b291-afb4-a65f-b8d5-e60db6003505.png)

ライブデモはこちらから参照

https://stackblitz.com/~/github.com/sey323/angular-dynamic-component-loader-sample


サンプルは各コンポーネントに引数が1つしかないシンプルないコンポーネントのため、複雑さは感じない。
しかし機能追加していき、`Input`や`Output`が増えていくと以下の課題が発生する

- 複数のコンポーネントでプロパティバインディング(`Input`や`Output`)が重複し、ソースコードが冗長になる
- 呼び出し元のソースコードの量が増えて見えづらくなる

具体的には以下の様に、引数が増えてソースコードの見通しが悪くなることが多々ある。

```html:src/app/app.component.html
<div>
  <h1>Programming Language</h1>

  <!-- 表示するページをセレクトボックスから選択する -->
  <select [(ngModel)]="selectedLanguage">
    <option [value]="programmingLanguageEnum.JAVA">Java</option>
    <option [value]="programmingLanguageEnum.JAVASCRIPT">JavaScript</option>
    <option [value]="programmingLanguageEnum.TYPESCRIPT">TypeScript</option>
  </select>

  <!-- セレクトボックスで選択した内容で表示するページを切り替える -->
  <div>
    @switch (selectedLanguage) {
      @case (programmingLanguageEnum.JAVA) {
        <app-java-page
            [arg1]="arg1"
            [arg2]="arg2"
            [arg3]="arg3"
            (func1)="func1"
            (func2)="func2"
            (func3)="func3"
        ></app-java-page>
      }
      @case (programmingLanguageEnum.JAVASCRIPT) {
          <app-javascript-page
            [arg1]="arg1"
            [arg2]="arg2"
            [arg3]="arg3"
            (func1)="func1"
            (func2)="func2"
            (func3)="func3"
          ></app-javascript-page>
      }
      @default {
        <app-typescript-page
            [arg1]="arg1"
            [arg2]="arg2"
            [arg3]="arg3"
            (func1)="func1"
            (func2)="func2"
            (func3)="func3"
        ></app-typescript-page>
      }
    }
  </div>
</div>
```

この課題に対して、Angularの動的コンポーネントローダーの機能を利用し解決する。

# Dynamic Component Loader(動的コンポーネントローダー)

Dynamic Component Loaderは、画面表示に利用するコンポーネントを一度に全て読み込むのではなく、必要に応じて読み込む仕組みである。
ReactのLazy Loadと似た仕組みである。

Dynamic Component Loadeを利用して得られるメリットとしては、以下が挙げられる

1. 条件付きレンダリング  
特定の条件でのみ表示するコンポーネントを、必要になったタイミングっで読み込むことができる

2. パフォーマンス改善  
1に関連し、必要になったタイミングで読み込むので、ロード時間の短縮、効率的なメモリ管理が可能となる。

https://angular.jp/guide/dynamic-component-loader

# 実装

サンプルのページをDynamic Component Loaderの機能を利用し、シンプルにしていく。

利用したソースコードはGithubからも確認可能

https://github.com/sey323/angular-dynamic-component-loader-sample

デモはこちらから

https://stackblitz.com/~/github.com/sey323/angular-dynamic-component-loader-sample

## 環境

- Angular v17.2

## 1. 表示したいコンポーネントのリストを定義

はじめに①で、画面に呼び出したいコンポーネントをリストとして宣言しておく。
そして②でセレクトボックスで選択したプログラミング言語に応じて、対応するコンポーネントなどを取得する。

```typescript:src/app/app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    RouterOutlet,
    JavaPageComponent,
    JavascriptPageComponent,
    TypescriptPageComponent,
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  title = 'component-di-sample';
  selectedLanguage = 'Java';

  programmingLanguageEnum = ProgrammingLanguage;

  // ①表示したいコンポーネントのリスト
  private programmingLanguageList = [
    {
      component: JavaPageComponent,
      key: ProgrammingLanguage.JAVA,
      inputs: {
        input: 'Javaのページ',
      },
    },
    {
      component: JavascriptPageComponent,
      key: ProgrammingLanguage.JAVASCRIPT,
      inputs: {
        input: 'JavaScriptのページ',
      },
    },
    {
      component: TypescriptPageComponent,
      key: ProgrammingLanguage.TYPESCRIPT,
      inputs: {
        input: 'TypeScriptのページ',
      },
    },
  ] as {
    component: Type<any>;
    key: ProgrammingLanguage;
    inputs: Record<string, unknown>;
  }[];

  // ②セレクトボックスで選択された言語のコンポーネントを取得
  get currentProgrammingLanguage() {
    return this.programmingLanguageList.find(
      (language) => language.key === this.selectedLanguage
    )!;
  }
}

```

## 2. Dynamic Component Loaderの設定

`ngComponentOutlet`ディレクティブを利用し、Dynamic Component Loaderの機能を利用する。

```html:src/app/app.component.html
<div>
  <h1>Programming Language</h1>

  <!-- 表示するページをセレクトボックスから選択する -->
  <select [(ngModel)]="selectedLanguage">
    <option [value]="programmingLanguageEnum.JAVA">Java</option>
    <option [value]="programmingLanguageEnum.JAVASCRIPT">JavaScript</option>
    <option [value]="programmingLanguageEnum.TYPESCRIPT">TypeScript</option>
  </select>

  <!-- セレクトボックスで選択した内容で表示するページを切り替える -->
  <div>
    <ng-container *ngComponentOutlet="
    currentProgrammingLanguage.component;
    inputs: currentProgrammingLanguage.inputs;
  "
  />
  </div>
</div>

```

`@Input`で渡している値は`src/app/app.component.ts`の①でコンポーネントを宣言する際に、`inputs`にkey-value形式で宣言する。

その他`ngComponentOutlet`で動的コンポーネント設定可能な値は、公式ドキュメントを参照。

https://angular.jp/api/common/NgComponentOutlet

# 注意: 動的コンポーネントに`@Output`のEventを渡すことができない

2024年2月18日現時点で、動的コンポーネントで表示したコンポーネントに、`@Output`のEventを渡すことができない

こちらに関しては以下のISSUEで議論が進められているので、将来的に採用される可能性はある。

https://github.com/angular/angular/issues/15360