Angularで動的にコンポーネントを追加する

ボタンをクリックした時に、Angularコンポーネントを動的に挿入する方法を記載します。

以下の2つに分けて解説しています。

1. Angularコンポーネントを動的に挿入（→ まずは場所を考えずに挿入します。）
2. 特定の場所に、Angularコンポーネントを動的に挿入

## Step1. Angularコンポーネントを動的に挿入

### 動的に挿入するコンポーネントを作成

ここでは、以下のようなとても簡単なコンポーネントを作成します。

```src/app/dynamic/dynamic.component.ts
...
import { Component } from '@angular/core';

@Component({
  selector: 'app-dynamic',
  template: `<p>dynamic works!</p>`
})
export class DynamicComponent {

}
```


### ボタンをhtml上に配置

ボタンをクリックすると、メソッド `onButtonClick()` が実行され、Angularコンポーネントを生成できるようにします。

```src/app/app.component.html
...
<div>
  <button (click)="onButtonClick()">click!</button>
</div>
```


### 動的にAngularコンポーネントを挿入

`ComponentFactory` 、 `viewContainerRef` を利用し、Angularコンポーネントを挿入します。

* 初期処理 `ngOnInit()` で、動的に `DynamicComponent` をコンパイルできる状態にします。
* クリックされた時に、 `viewContainerRef.createComponent` を利用し、コンポーネント生成を実行します。

```src/app/app.component.ts
...
import { Component, ComponentFactory, ComponentFactoryResolver, OnInit, ViewContainerRef } from '@angular/core';
import { DynamicComponent } from './dynamic/dynamic.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  factory: ComponentFactory<DynamicComponent>;

  constructor(
    public viewContainerRef: ViewContainerRef,
    private resolver: ComponentFactoryResolver) { }

  ngOnInit() {
    this.factory = this.resolver.resolveComponentFactory(DynamicComponent);
  }

  onButtonClick() {
    this.viewContainerRef.createComponent(this.factory);
  }
}
```

### `@NgModule.entryComponents` に、動的に挿入するコンポーネントを追加

(3)まで実装し、ボタンをクリックすると、`ERROR Error: No component factory found for DynamicComponent. Did you add it to @NgModule.entryComponents?` というエラーが表示されます。

指示される通りに `entryComponents` を追加すると、ボタンを押すと動的にAngularコンポーネントを表示するところまで完成します。

```src/app/app.module.ts
...
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { DynamicComponent } from './dynamic/dynamic.component';


@NgModule({
  declarations: [
    AppComponent,
    DynamicComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents: [DynamicComponent] // Add entryComponents here!
})
export class AppModule { }
```

ソース全体の確認は[こちら](https://stackblitz.com/edit/playground-dynamic-component)

## Step2. 特定の場所に、Angularコンポーネントを動的に挿入

（Step1 が完了している前提で） `@ViewChild` を利用することで、コンポーネントを置く場所を指定することが可能になります。component.tsとhtmlをそれぞれ編集します。

```src/app/app.component.ts
...
export class AppComponent implements OnInit {
  factory: ComponentFactory<DynamicComponent>;
  // @ViewChild を追加
  @ViewChild('dynamic', { read: ViewContainerRef }) viewContainerRef: ViewContainerRef;

  // 不要になった ViewContainerRef をコンストラクタから削除
  constructor(
    private resolver: ComponentFactoryResolver) { }

（以下略）
```

htmlには、コンポーネントを挿入したい箇所に、上記の `@ViewChild` 定義した `#dynamic` タグを追加します。以下では、ボタンの上にコンポーネントが生成されるようにします。

```src/app/app.component.html
...
<div #dynamic></div>
<div>
  <button (click)="onButtonClick()">click!</button>
</div>
```

上記を完了したうえでボタンをクリックすると、ボタンの上に"dynamic works!"と表示されることが確認できます。

ソース全体の確認は[こちら](https://stackblitz.com/edit/playground-dynamic-component2)

## 補足
* 今回の実装ではDOM要素を追加しているだけなので、`DomSanitizer`を利用して実装することもできます。
* コンポーネントを生成するロジックをServiceクラスに置くことで、ほかのコンポーネントから呼び出すことができるようにできます。詳しくは「参考」に載せているページで確認できます。

## 参考
* [Dynamically add Component in angular 2/4 - Stack Overflow](https://stackoverflow.com/questions/49079989/dynamically-add-component-in-angular-2-4)
* [Angularで中身を動的に変えられるModalを作る【基本編】 - Carpe Diem](https://christina04.hatenablog.com/entry/2017/02/18/184241)
* [Dynamically add components to the DOM with Angular – Frontend Weekly – Medium](https://medium.com/front-end-weekly/dynamically-add-components-to-the-dom-with-angular-71b0cb535286)
* [ionic framework - What is entryComponents in angular ngModule? - Stack Overflow](https://stackoverflow.com/questions/39756192/what-is-entrycomponents-in-angular-ngmodule)