Angular で動的にコンポーネントを生成し画面に挿入する

Angular に Compiler#compileModuleAndAllComponentsSync() というメソッドがあるのを知り、ちょっと遊んでみた。

コレは動的に NgModule とコンポーネントを生成できるシロモノで、「画面から入力された HTML ソースを基に Angular コンポーネントとしてコンパイルする」なんてことまでできてしまった。

以下、app.component.ts で実装したサンプルソース全量。[(ngModel)] の使用箇所があるため、app.module.ts では FormsModuleimports しておくこと。

import { CommonModule } from '@angular/common';
import { Compiler, Component, NgModule, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: `
    <p><textarea [(ngModel)]="htmlStr" placeholder="ココに HTML を入力してください"></textarea></p>
    <p><button (click)="compileHtml(htmlStr)">コンパイル開始</button></p>
    <p>コンパイル結果 :</p>
    <div style="border: 2px dashed blue; padding: 15px;">
      <div #dest></div>
    </div>
  `
})
export class AppComponent {
  /** コンポーネントの実装を受け取るテキストエリアの値 */
  public htmlStr: string = '';
  
  /**
   * ダイナミックコンテンツを挿入する子コンポーネントの参照を定義しておく
   * HTML 側では @ViewChild の第2引数に指定した文字列で <div #dest></div> のように配置する要素を用意しておく
   */
  @ViewChild('dest', { read: ViewContainerRef })
  public dest: ViewContainerRef;
  
  /**
   * コンストラクタ
   * 
   * @param compiler コンパイラ
   */
  constructor(private compiler: Compiler) { }
  
  /**
   * 引数の文字列からコンポーネントを生成し親コンポーネントに挿入する
   * 
   * @param htmlStr this.htmlStr を渡す
   */
  public directInjection(htmlStr: string): void {
    // Angular コンポーネントを用意する
    @Component({
      selector: 'app-temp',
      // 「this.htmlStr」とは指定できない。引数で文字列を渡す
      template: htmlStr
    })
    class TempComponent { }
    // Angular モジュールを用意する
    @NgModule({
      imports: [CommonModule],
      declarations: [TempComponent],
      exports: [TempComponent]
    })
    class TempModule { }
    
    // 元の要素をクリアする
    this.dest.clear();
    
    // Angular モジュールとコンポーネントをコンパイルする
    const module = this.compiler.compileModuleAndAllComponentsSync(TempModule);
    // Angular モジュールから TempComponent を生成するファクトリを取り出す
    const factory = module.componentFactories.find((c) => {
      return c.componentType === TempComponent;
    });
    // TempComponent を生成し this.dest に設定する
    this.dest.createComponent(factory);
  }
}

こうして出来た AppComponent を見ると、テキストエリアと「コンパイル開始」ボタン、そして初期状態では空の青い枠線が見えるだろう。テキストエリアに

<style>button { font-size; 200%; }</style>
<p style="background: #ff0;">
  <button onclick="alert('Test!')">Example</button>
</p>

このような HTML コードを書いて「コンパイル開始」ボタンを押すと、青枠の中に大きな button 要素が表示され、クリックするとダイアログが表示されるだろう。Angular コンポーネントとして正しくコンパイルされている。

実際はユーザから受け取った HTML ソースをコンパイルするようなことは少なく、テンプレート HTML を動的に切り替えるような使い方になるかと。

結局は NgModule と Component なので、TempModule で何かライブラリを imports しておき、TempComponent に sample(event) メソッドなどを用意しておけば、<button (click)="sample($event)">ボタン</button> といったテンプレート HTML をコンパイルして実行させることもできる。

<div #dest></div> のようにコンポーネント (テンプレート) の出力先を予め実装しておく必要があるので、中々万能ではないが、面白い機能だ。