1. ホーム
  2. アンギュラー

[解決済み] Angular 2.0で動的コンポーネントをコンパイルするために動的テンプレートを使用/作成するにはどうすればよいですか?

2022-04-17 03:09:31

質問

テンプレートを動的に作成したい。これを利用して ComponentType を実行時に配置し (置き換えも) をホスティングしているComponentの中のどこかに入れてください。

RC4までは、私は ComponentResolver が、RC5では以下のようなメッセージが表示されます。

ComponentResolver is deprecated for dynamic compilation.
Use ComponentFactoryResolver together with @NgModule/@Component.entryComponents or ANALYZE_FOR_ENTRY_COMPONENTS provider instead.
For runtime compile only, you can also use Compiler.compileComponentSync/Async.

こんなドキュメントがありました( Angular 2 同期ダイナミックコンポーネントの作成 )

そして、私はどちらかを使用することができることを理解します。

  • 動的な種類 ngIfComponentFactoryResolver . の中で既知のコンポーネントを渡すと @Component({entryComponents: [comp1, comp2], ...}) - を使うことができますね。 .resolveComponentFactory(componentToRender);
  • リアルランタイムコンパイルで Compiler ...

しかし、問題はそれをどう使うかです Compiler ? 上の注意書きには、「呼び出す」と書いてあります。 Compiler.compileComponentSync/Async - で、どうやって?

例えば を作りたい。 (いくつかの設定条件に基づいて) このようなテンプレートは、ある種の設定に対して

<form>
   <string-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></string-editor>
   <string-editor
     [propertyName]="'description'"
     [entity]="entity"
   ></string-editor>
   ...

そして、別のケースでは、このようなものです。 ( string-editor に置き換えられます。 text-editor )

<form>
   <text-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></text-editor>
   ...

などなど (異なる番号/日付/参照 editors プロパティの種類によって、ユーザーによってはいくつかのプロパティをスキップしてしまう...) つまり、これは一例であり、実際の構成ではもっと異なる複雑なテンプレートが生成される可能性があります。

テンプレートが変更されるため ComponentFactoryResolver と既存のものを渡す・・・。を使った解決策が必要です。 Compiler .

解決方法は?

EDIT - 関連 2.3.0 (2016-12-07)

<ブロッククオート

注意:以前のバージョンの解決策を得るには、この投稿の履歴を確認してください。

似たような話題はこちら Angular 2 の $compile に相当するものです。 . 私たちは JitCompilerNgModule . についてもっと読む NgModule をAngular2で使用することができます。

ひとことで言うと

があります。 動作するプランカー/サンプル (ダイナミックテンプレート、ダイナミックコンポーネントタイプ、ダイナミックモジュール。 JitCompiler , ...実行中)

プリンシパルは

1) テンプレート作成

2) 見つける ComponentFactory をキャッシュに保存します。 行く 7)

3) - 作成 Component

4)-作成 Module

5) - コンパイル Module

6) - リターン (そして後で使うためにキャッシュ) ComponentFactory

7) 使用 対象 ComponentFactory のインスタンスを作成し、動的な Component

以下はコードの断片です。 (さらに こちら ) - カスタムビルダーは、ビルド/キャッシュされたものだけを返します。 ComponentFactory のインスタンスを作成するために、ビューターゲットプレースホルダーを消費します。 DynamicComponent

  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });

簡単に言うと、これだけです。もっと詳しく知りたい方は、以下をお読みください。

.

TL&DR

プランカーを観察し、詳細な説明が必要なスニペットがある場合は、戻って詳細を読むことができます。

.

詳細説明 - Angular2 RC6++ & ランタイムコンポーネント

の説明の下 本シナリオ を行う予定です。

  1. モジュールを作成する PartsModule:NgModule (小物入れ)
  2. 別のモジュールを作成する DynamicModule:NgModule このコンポーネントには、ダイナミックコンポーネント (そして PartsModule を動的に実行します)。
  3. 動的テンプレート作成 (簡単な方法)
  4. 新規作成 Component タイプ (テンプレートが変更された場合のみ)
  5. 新規作成 RuntimeModule:NgModule . このモジュールには、以前に作成した Component タイプ
  6. コール JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) を取得する。 ComponentFactory
  7. のインスタンスを作成します。 DynamicComponent - ビューターゲットプレースホルダーのジョブと ComponentFactory
  8. 割り当てる @Inputs 新しいインスタンス (から切り替える)。 INPUT から TEXTAREA 編集) 消費する @Outputs

NgModule

を必要とします。 NgModule s.

非常にシンプルな例を示したいと思いますが、この場合、3つのモジュールが必要になります。 (実際には4つ。ただしAppModuleは数えない) . これを見てください 単純なスニペットではなく をベースに、本当にしっかりとした動的コンポーネントジェネレータを作ることができます。

があります。 モジュールは、すべての小さなコンポーネント、例えば string-editor , text-editor ( date-editor , number-editor ...)

@NgModule({
  imports:      [ 
      CommonModule,
      FormsModule
  ],
  declarations: [
      DYNAMIC_DIRECTIVES
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule
  ]
})
export class PartsModule { }

どこ DYNAMIC_DIRECTIVES は拡張可能で、ダイナミックコンポーネントテンプレート/タイプに使用されるすべての小さなパーツを保持することを目的としています。チェック app/parts/parts.module.ts

2つ目は、Dynamicなものを扱うためのモジュールになります。これはホスティングコンポーネントといくつかのプロバイダを含みますが、これらはシングルトンになります。そのため、標準的な方法で公開することになります。 forRoot()

import { DynamicDetail }          from './detail.view';
import { DynamicTypeBuilder }     from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';

@NgModule({
  imports:      [ PartsModule ],
  declarations: [ DynamicDetail ],
  exports:      [ DynamicDetail],
})

export class DynamicModule {

    static forRoot()
    {
        return {
            ngModule: DynamicModule,
            providers: [ // singletons accross the whole app
              DynamicTemplateBuilder,
              DynamicTypeBuilder
            ], 
        };
    }
}

の使い方を確認します。 forRoot() の中で AppModule

最後に、アドホックなランタイムモジュールが必要になるが、これは後で DynamicTypeBuilder ジョブがあります。

4番目のモジュールであるアプリケーション・モジュールは、コンパイラ・プロバイダの宣言を継続するものです。

...
import { COMPILER_PROVIDERS } from '@angular/compiler';    
import { AppComponent }   from './app.component';
import { DynamicModule }    from './dynamic/dynamic.module';

@NgModule({
  imports:      [ 
    BrowserModule,
    DynamicModule.forRoot() // singletons
  ],
  declarations: [ AppComponent],
  providers: [
    COMPILER_PROVIDERS // this is an app singleton declaration
  ],

読む (読み取りを行う) より多くの NgModule をご覧ください。

A テンプレート ビルダー

この例では、このような エンティティ

entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
};

を作成するには template は、この中で プランカー このシンプルで素朴なビルダーを使っています。

<ブロッククオート

本当の解決策、本当のテンプレートビルダーは、あなたのアプリケーションが多くのことをできる場所です。

// plunker - app/dynamic/template.builder.ts
import {Injectable} from "@angular/core";

@Injectable()
export class DynamicTemplateBuilder {

    public prepareTemplate(entity: any, useTextarea: boolean){
      
      let properties = Object.keys(entity);
      let template = "<form >";
      let editorName = useTextarea 
        ? "text-editor"
        : "string-editor";
        
      properties.forEach((propertyName) =>{
        template += `
          <${editorName}
              [propertyName]="'${propertyName}'"
              [entity]="entity"
          ></${editorName}>`;
      });
  
      return template + "</form>";
    }
}

ここでのトリックは - 既知のプロパティのセットを使用するテンプレートを構築することです。 entity . このようなプロパティ(-ies)は、次に作成するダイナミック・コンポーネントの一部である必要があります。

もう少し簡単にするために、テンプレート・ビルダーが使用できるプロパティを定義するインターフェイスを使用することができます。これは、ダイナミックコンポーネントタイプで実装されます。

export interface IHaveDynamicData { 
    public entity: any;
    ...
}

A ComponentFactory ビルダー

ここで非常に重要なのが、「心得」です。

コンポーネントタイプ、ビルドは DynamicTypeBuilder しかし、そのテンプレートが異なるだけです。 (上記で作成した) . コンポーネントのプロパティ (入力、出力、またはいくつかの protected)はそのままです。 異なるプロパティが必要な場合、テンプレートとタイプビルダーの異なる組み合わせを定義する必要があります。

つまり、私たちのソリューションの核心に触れているのです。Builderは、1) ComponentType 2) その NgModule 3) コンパイル ComponentFactory 4) キャッシュ を後で再利用できるようにします。

受け取る必要のある依存関係。

// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
    
@Injectable()
export class DynamicTypeBuilder {

  // wee need Dynamic component builder
  constructor(
    protected compiler: JitCompiler
  ) {}

そして、以下がそのスニペットです。 ComponentFactory :

// plunker - app/dynamic/type.builder.ts
// this object is singleton - so we can use this as a cache
private _cacheOfFactories:
     {[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};
  
public createComponentFactory(template: string)
    : Promise<ComponentFactory<IHaveDynamicData>> {    
    let factory = this._cacheOfFactories[template];

    if (factory) {
        console.log("Module and Type are returned from cache")
       
        return new Promise((resolve) => {
            resolve(factory);
        });
    }
    
    // unknown template ... let's create a Type for it
    let type   = this.createNewComponent(template);
    let module = this.createComponentModule(type);
    
    return new Promise((resolve) => {
        this.compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                factory = _.find(moduleWithFactories.componentFactories
                                , { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });
}

上記で作成し キャッシュ ともに ComponentModule . なぜなら、もしテンプレート (実際には、そのすべての本当の動的な部分) が同じであれば、再利用が可能です。

そして、ここに2つのメソッドがあります。 装飾された クラス/タイプを実行時に表示します。また @Component が、さらに @NgModule

protected createNewComponent (tmpl:string) {
  @Component({
      selector: 'dynamic-component',
      template: tmpl,
  })
  class CustomDynamicComponent  implements IHaveDynamicData {
      @Input()  public entity: any;
  };
  // a component for this particular template
  return CustomDynamicComponent;
}
protected createComponentModule (componentType: any) {
  @NgModule({
    imports: [
      PartsModule, // there are 'text-editor', 'string-editor'...
    ],
    declarations: [
      componentType
    ],
  })
  class RuntimeComponentModule
  {
  }
  // a module for just this Type
  return RuntimeComponentModule;
}

重要です。

のコンポーネントの動的型は、テンプレートによって異なるだけです。そこで、その事実を利用します。 をキャッシュするために を使用します。これは本当にとても重要なことです。 Angular2でもキャッシュされます。 によって、これらの タイプ . そして、同じテンプレート文字列に対して新しい型を作り直すと、メモリリークが発生するようになります。

ComponentFactory ホスティングコンポーネントで使用される

最後の部品は、ダイナミックコンポーネントのターゲットをホストするコンポーネントです。 <div #dynamicContentPlaceHolder></div> . その参照を取得し ComponentFactory を使用してコンポーネントを作成します。これは簡単に言うと、そのコンポーネントのすべてのピースは次のとおりです。 (必要であれば、オープン プランカーはこちら )

まず、import文についてまとめましょう。

import {Component, ComponentRef,ViewChild,ViewContainerRef}   from '@angular/core';
import {AfterViewInit,OnInit,OnDestroy,OnChanges,SimpleChange} from '@angular/core';

import { IHaveDynamicData, DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder }               from './template.builder';

@Component({
  selector: 'dynamic-detail',
  template: `
<div>
  check/uncheck to use INPUT vs TEXTAREA:
  <input type="checkbox" #val (click)="refreshContent(val.checked)" /><hr />
  <div #dynamicContentPlaceHolder></div>  <hr />
  entity: <pre>{{entity | json}}</pre>
</div>
`,
})
export class DynamicDetail implements AfterViewInit, OnChanges, OnDestroy, OnInit
{ 
    // wee need Dynamic component builder
    constructor(
        protected typeBuilder: DynamicTypeBuilder,
        protected templateBuilder: DynamicTemplateBuilder
    ) {}
    ...

テンプレートとコンポーネントビルダーを受け取ったところです。次に、この例で必要となるプロパティです。 (詳細はコメントにて)

// reference for a <div> with #dynamicContentPlaceHolder
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef}) 
protected dynamicComponentTarget: ViewContainerRef;
// this will be reference to dynamic content - to be able to destroy it
protected componentRef: ComponentRef<IHaveDynamicData>;

// until ngAfterViewInit, we cannot start (firstly) to process dynamic stuff
protected wasViewInitialized = false;

// example entity ... to be recieved from other app parts
// this is kind of candiate for @Input
protected entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
  };

この単純なシナリオでは、ホスティングコンポーネントには @Input . そのため、変更に反応する必要がありません。しかし、そのような事実にもかかわらず (来るべき変化に備えるため)。 - コンポーネントがすでに が開始されます。そうして初めて、私たちは魔法を始めることができるのです。

最後にコンポーネントビルダーを使用し、その をコンパイル/キャッシュしただけの ComponentFacotry . 私たちの ターゲット・プレースホルダー をインスタンス化するように要求されます。 その Component その工場で

protected refreshContent(useTextarea: boolean = false){
  
  if (this.componentRef) {
      this.componentRef.destroy();
  }
  
  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });
}

スモールエクステンション

また、コンパイルされたテンプレートへの参照を保持する必要があります。 destroy() を変更するときは、いつでも変更できます。

// this is the best moment where to start to process dynamic stuff
public ngAfterViewInit(): void
{
    this.wasViewInitialized = true;
    this.refreshContent();
}
// wasViewInitialized is an IMPORTANT switch 
// when this component would have its own changing @Input()
// - then we have to wait till view is intialized - first OnChange is too soon
public ngOnChanges(changes: {[key: string]: SimpleChange}): void
{
    if (this.wasViewInitialized) {
        return;
    }
    this.refreshContent();
}

public ngOnDestroy(){
  if (this.componentRef) {
      this.componentRef.destroy();
      this.componentRef = null;
  }
}

完了

これでほぼ終了です。を忘れないでください。 破壊する 動的に構築されたものは (ngOnDestroy) . また、必ず キャッシュ ダイナミック typesmodules が、テンプレートだけの違いであれば

すべての動作を確認する こちら

<ブロッククオート

以前のバージョンを見るには (例:RC5関連) をご覧ください。 歴史