動的テンプレートを使用/作成して、Angular 2.0で動的コンポーネントをコンパイルするにはどうすればよいですか?


197

テンプレートを動的に作成したい。これはComponentType、実行時にat を構築し、それをホストコンポーネント内のどこかに配置(置き換えることも)するために使用する必要があります。

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 Synchronous Dynamic Component Creation

そして、私はどちらかを使用できることを理解してください

  • ダイナミックの種類ngIfComponentFactoryResolver。内部に既知のコンポーネントを渡した場合@Component({entryComponents: [comp1, comp2], ...})-使用できます.resolveComponentFactory(componentToRender);
  • 実際のランタイムコンパイル、Compiler...

しかし、問題はそれをどのように使用するCompilerかです。上記のメモには、次のように電話する必要があると記載されCompiler.compileComponentSync/Asyncています。

例えば。(いくつかの構成条件に基づいて) 1種類の設定用にこの種のテンプレートを作成したい

<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です。


私が見つけた解決策はとても良かったので、この質問を見つけた全員に、現時点で一番下にある私の答えを見てもらいたいのです。:)
Richard Houltz


ここにすべての回答の問題と$compile、これらのメソッドでは実際に実行できないことを示します。私は、サードパーティのページとajax呼び出しを介してHTMLをコンパイルするだけのアプリケーションを作成しています。ページからHTMLを削除して自分のテンプレートに配置することはできません。ため息
Augie Gardner 2017

@AugieGardnerこれが設計上不可能である理由があります。Angularは、一部の人々が持っている悪いアーキテクチャの決定やレガシーシステムに責任はありません。既存のHTMLコードを解析したい場合は、AngularがWebComponentsで完璧に機能するため、別のフレームワークを自由に使用できます。経験の浅いプログラマーの大群を導く明確な境界を設定することは、いくつかのレガシーシステムにダーティハックを許可することよりも重要です。
Phil

回答:


163

EDIT -に関連2.3.0(2016年12月7日)

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

同様のトピックがここで説明されています。Angular2の$ compileに相当します。とを使用する必要がJitCompilerありNgModuleます。NgModuleAngular2の詳細については、こちらをご覧ください。

一言で言えば

ある作業plunker /例 (動的テンプレート、動的コンポーネントタイプ、動的モジュールは、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. のインスタンスを作成する-View DynamicComponentTargetプレースホルダーのジョブとComponentFactory
  8. 割り当て@Inputs新しいインスタンス (からスイッチINPUTTEXTAREA編集)、消費@Outputs

NgModule

NgModules が必要です。

非常に単純な例を示したいのですが、この場合、3つのモジュールが必要になります(実際には4つですが、AppModuleはカウントしません)単純なスニペットではなく、これを、非常に堅実な動的コンポーネントジェネレーターの基礎として使用してください。

...)など、すべての小さなコンポーネントに対して1つのモジュールがあります。string-editortext-editor date-editornumber-editor

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

どこにDYNAMIC_DIRECTIVES拡張可能であり、私たちのダイナミックなコンポーネントテンプレート/タイプのために使用されるすべての小さな部品を保持することを意図しています。app / parts / parts.module.tsを確認します

2つ目は、動的スタッフ処理用のモジュールです。ホスティングコンポーネントといくつかのプロバイダーが含まれます。シングルトンになります。そのため、私たちはそれらを標準的な方法で公開します-と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についてもっと読んでください(読んでください)

テンプレートビルダー

この例では、この種のエンティティの詳細を処理します

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です。そのようなプロパティは、次に作成する動的コンポーネントの一部である必要があります。

もう少し簡単にするために、テンプレートビルダーで使用できるインターフェイスを使用してプロパティを定義できます。これは、動的コンポーネントタイプによって実装されます。

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

ComponentFactoryビルダー

ここで非常に重要なことは、覚えておくことです。

で構築するコンポーネントタイプDynamicTypeBuilderは異なる可能性がありますが、そのテンプレート(上記で作成)のみが異なります。コンポーネントのプロパティ(入力、出力、または一部の保護)は同じです。異なるプロパティが必要な場合は、テンプレートとタイプビルダーの異なる組み合わせを定義する必要があります

したがって、私たちはソリューションのコアに触れています。ビルダーは、1)作成ComponentType2)その作成NgModule3)コンパイルComponentFactory4)後で再利用するためにキャッシュします。

受け取る必要がある依存関係:

// 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);
            });
    });
}

上記では、およびの両方を作成してキャッシュComponentていModuleます。テンプレート(実際にはそのすべての実際の動的な部分)が同じである場合は、再利用できます。

そして、ここに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 {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)。また、必ずキャッシュダイナミックtypesかつmodules唯一の違いは、そのテンプレートである場合。

すべての動作をここで確認してください

この投稿の以前のバージョン(RC5関連など)を表示するには、履歴を確認してください


50
これはそのような複雑なソリューションのように見えますが、廃止予定のソリューションは非常にシンプルで明確でした。これを行う他の方法はありますか?
tibbus

3
@tibbusと同じ方法だと思います。これは、廃止されたコードを使用する場合よりも複雑になりました。回答ありがとうございます。
Lucio Mollinedo 16

5
@ribsiesご連絡ありがとうございます。はっきりさせておきます。他の多くの答えはそれを簡単にしようとします。しかし、私はそれを説明し、実際の使用に近いシナリオでそれを示すことを試みています。キャッシュする必要があります。再作成時にdestroyを呼び出す必要があります。したがって、動的構築の魔法は実際にtype.builder.tsあなたが指摘したとおりですが、すべてのユーザーがそれをすべてに配置する方法を理解できるようにしたいと思いますコンテキスト...それが役に立つことを願っています;)
RadimKöhlerOct

7
@RadimKöhler-この例を試しました。AOTがなくても機能します。しかし、AOTでこれを実行しようとすると、「RuntimeComponentModuleのNgModuleメタデータが見つかりません」というエラーが表示されます。このエラーを解決するのに役立ちますか?
Trusha

4
答え自体は完璧です!しかし、実際のアプリケーションでは実用的ではありません。これはビジネスアプリケーションの一般的な要件であるため、角度のあるチームは、フレームワークでこれに対するソリューションを提供する必要があります。そうでない場合は、Angular 2がビジネスアプリケーションに適したプラットフォームかどうかを尋ねる必要があります。
Karl

58

編集(2017年8月26日):以下のソリューションは、Angular2および4で適切に機能します。テンプレート変数とクリックハンドラーを含むように更新し、Angular 4.3でテストしました。
Angular4の場合、Ophirの回答で説明されているngComponentOutlet がはるかに優れたソリューションです。ただし、現時点では入力と出力はまだサポートされいません。[このPR](https://github.com/angular/angular/pull/15362]が受け入れられた場合、createイベントによって返されたコンポーネントインスタンスを介してそれが可能になります
。ng-dynamic-componentが最適で最も簡単な場合があります完全に解決策ですが、まだテストしていません。

@Long Fieldの答えは正解です。次に、別の(同期)例を示します。

import {Compiler, Component, NgModule, OnInit, ViewChild,
  ViewContainerRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `<h1>Dynamic template:</h1>
             <div #container></div>`
})
export class App implements OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private compiler: Compiler) {}

  ngOnInit() {
    this.addComponent(
      `<h4 (click)="increaseCounter()">
        Click to increase: {{counter}}
      `enter code here` </h4>`,
      {
        counter: 1,
        increaseCounter: function () {
          this.counter++;
        }
      }
    );
  }

  private addComponent(template: string, properties?: any = {}) {
    @Component({template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
    Object.assign(component.instance, properties);
    // If properties are changed at a later stage, the change detection
    // may need to be triggered manually:
    // component.changeDetectorRef.detectChanges();
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

http://plnkr.co/edit/fdP9Ocに住んでいます。


3
私はそれを書く方法の例であること、言うだろうできるだけ少ないコードとしてやって私の答えの中と同じstackoverflow.com/a/38888009/1679310を。場合によっては、状態が変化したときに便利なケース(ほとんどがREを生成するテンプレート)である必要があります...を使用した単純なngAfterViewInit呼び出しconst templateは機能しません。しかし、上記の詳細なアプローチ(テンプレートの作成、コンポーネントの作成、モジュールの作成、コンパイル、ファクトリの作成、インスタンスの作成)を減らすことがタスクである場合、おそらくそれを行いました
RadimKöhlerSep

解決策をありがとう:templateUrlとスタイルの読み込みに問題がありますが、次のエラーが発生します。ResourceLoader実装が提供されていません。URL localhost:3000 / app / pages / pages_common.cssを読み取ることができません。何が欠けているのでしょうか?
Gerardlamo

コントロールのようなグリッドのセルに固有のデータを使用してhtmlテンプレートをコンパイルすることは可能ですか?plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=previewこのプランカーでは、をどのようにコンパイルして、最後の列に画像を表示できますか?助けて?
Karthick 2016年

1
@monnef、あなたは正しい。コンソールログは確認しませんでした。ngAfterViewInitフックではなくngOnInitにコンポーネントを追加するようにコードを調整しました。前者は変更検出の前にトリガーされ、後者は変更検出後にトリガーされるためです。(github.com/angular/angular/issues/10131および同様のスレッドを参照してください。)
ルネハンバーガー

1
端正でシンプル。devでブラウザーを介して提供するときに期待どおりに機能しました。しかし、これはAOTで機能しますか?コンパイル後にPRODでアプリを実行すると、コンポーネントのコンパイルが試行されたときに「エラー:ランタイムコンパイラが読み込まれていません」というメッセージが表示されます。(ところで、私はIonic 3.5を使用しています)
mymo 2017

52

私はパーティーに遅く到着したに違いありませんが、ここでの解決策はどれも私には役に立たないようでした-面倒すぎ、回避策が多すぎるように感じました。

私がやったことはAngular 4.0.0-beta.6ngComponentOutletを使用することです

これにより、すべてが動的コンポーネントのファイルに記述された、最も短くてシンプルなソリューションが得られました。

  • これは、テキストを受け取ってテンプレートに配置するだけの簡単な例ですが、明らかに必要に応じて変更できます。
import {
  Component, OnInit, Input, NgModule, NgModuleFactory, Compiler
} from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<ng-container *ngComponentOutlet="dynamicComponent;
                            ngModuleFactory: dynamicModule;"></ng-container>`,
  styleUrls: ['my.component.css']
})
export class MyComponent implements OnInit {
  dynamicComponent;
  dynamicModule: NgModuleFactory<any>;

  @Input()
  text: string;

  constructor(private compiler: Compiler) {
  }

  ngOnInit() {
    this.dynamicComponent = this.createNewComponent(this.text);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));
  }

  protected createComponentModule (componentType: any) {
    @NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
  }

  protected createNewComponent (text:string) {
    let template = `dynamically created template with text: ${text}`;

    @Component({
      selector: 'dynamic-component',
      template: template
    })
    class DynamicComponent implements OnInit{
       text: any;

       ngOnInit() {
       this.text = text;
       }
    }
    return DynamicComponent;
  }
}
  • 簡単な説明:
    1. my-component -動的コンポーネントがレンダリングしているコンポーネント
    2. DynamicComponent -動的に構築されるコンポーネントであり、my-component内でレンダリングされます

すべての角度ライブラリを^ Angular 4.0.0にアップグレードすることを忘れないでください

これがお役に立てば幸いです。

更新

角度5でも機能します。


3
これは、Angular4で私にとってはうまくいきました。私がしなければならない唯一の調整は、動的に作成されたRuntimeComponentModuleのインポートモジュールを指定できるようにすることでした。
Rahul Patel

8
Angularクイックスタートからの簡単な例を次に示します:embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel

5
このソリューションは「ng build --prod」で動作しますか?コンパイラクラスとAoTがatmに適合しないようです。
Pierre Chavaroche

2
@OphirStern私はまた、このアプローチがAngular 5ではうまく機能するが、-prodビルドフラグでは機能しないことも発見しました。
TaeKwonJ​​oe 2018

2
JitCompilerFactoryを使用して角度5(5.2.8)でテストし、-prodフラグを使用しても機能しません!誰かが解決策を持っていますか?(BTW JitCompilerFactoryに--prodフラグがないと問題なく動作します)
Frank

20

2019 6月の答え

素晴らしいニュース!@ angular / cdkパッケージはポータルのファーストクラスのサポートを持っているようです

執筆時点では、上記の公式ドキュメントは特に役立ちませんでした(特に、動的コンポーネントへのデータの送信と動的コンポーネントからのイベントの受信に関して)。要約すると、次のことを行う必要があります。

ステップ1) AppModule

パッケージPortalModuleからインポートし、@angular/cdk/portal内部に動的コンポーネントを登録しますentryComponents

@NgModule({
  declarations: [ ..., AppComponent, MyDynamicComponent, ... ]
  imports:      [ ..., PortalModule, ... ],
  entryComponents: [ ..., MyDynamicComponent, ... ]
})
export class AppModule { }

ステップ2.オプションA:動的コンポーネントにデータを渡したり、動的コンポーネントからイベントを受け取ったりする必要がない場合

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add child component</button>
    <ng-template [cdkPortalOutlet]="myPortal"></ng-template>
  `
})
export class AppComponent  {
  myPortal: ComponentPortal<any>;
  onClickAddChild() {
    this.myPortal = new ComponentPortal(MyDynamicComponent);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child.</p>`
})
export class MyDynamicComponent{
}

実際に見る

ステップ2.オプションB:動的コンポーネントにデータを渡したり、動的コンポーネントからイベントを受け取ったりする必要がある場合

// A bit of boilerplate here. Recommend putting this function in a utils 
// file in order to keep your component code a little cleaner.
function createDomPortalHost(elRef: ElementRef, injector: Injector) {
  return new DomPortalHost(
    elRef.nativeElement,
    injector.get(ComponentFactoryResolver),
    injector.get(ApplicationRef),
    injector
  );
}

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add random child component</button>
    <div #portalHost></div>
  `
})
export class AppComponent {

  portalHost: DomPortalHost;
  @ViewChild('portalHost') elRef: ElementRef;

  constructor(readonly injector: Injector) {
  }

  ngOnInit() {
    this.portalHost = createDomPortalHost(this.elRef, this.injector);
  }

  onClickAddChild() {
    const myPortal = new ComponentPortal(MyDynamicComponent);
    const componentRef = this.portalHost.attach(myPortal);
    setTimeout(() => componentRef.instance.myInput 
      = '> This is data passed from AppComponent <', 1000);
    // ... if we had an output called 'myOutput' in a child component, 
    // this is how we would receive events...
    // this.componentRef.instance.myOutput.subscribe(() => ...);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child. <strong>{{myInput}}</strong></p>`
})
export class MyDynamicComponent {
  @Input() myInput = '';
}

実際に見る


1
おい、あなたはちょうど釘付けにしました。これは注目されます。Angularに単純な動的コンポーネントを追加する必要があるのは、それが必要になるまで、どれほど難しいか信じられませんでした。これは、リセットを行ってJQuery以前の時間に戻るようなものです。
Gi1ber7

2
@ Gi1ber7わかってる?なぜこんなに長くかかったのですか?
スティーブンポール

1
素晴らしいアプローチですが、ChildComponentにパラメーターを渡す方法を知っていますか?
スヌーク

1
これは、あなたの質問に答えることが@Snook stackoverflow.com/questions/47469844/...
スティーブン・ポール・

4
@StephenPaulどのようにこれはないPortalアプローチが異なるngTemplateOutletngComponentOutlet?🤔
グレン・モハマド

18

学んだことをすべて1つのファイルに圧縮することにしました。特にRC5以前と比較して、ここで取り入れるべきことがたくさんあります。このソースファイルにはAppModuleとAppComponentが含まれていることに注意してください。

import {
  Component, Input, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
  OnInit, ViewChild
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

@Component({
  selector: 'app-dynamic',
  template: '<h4>Dynamic Components</h4><br>'
})
export class DynamicComponentRenderer implements OnInit {

  factory: ModuleWithComponentFactories<DynamicModule>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }

  ngOnInit() {
    if (!this.factory) {
      const dynamicComponents = {
        sayName1: {comp: SayNameComponent, inputs: {name: 'Andrew Wiles'}},
        sayAge1: {comp: SayAgeComponent, inputs: {age: 30}},
        sayName2: {comp: SayNameComponent, inputs: {name: 'Richard Taylor'}},
        sayAge2: {comp: SayAgeComponent, inputs: {age: 25}}};
      this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
        .then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
          this.factory = moduleWithComponentFactories;
          Object.keys(dynamicComponents).forEach(k => {
            this.add(dynamicComponents[k]);
          })
        });
    }
  }

  addNewName(value: string) {
    this.add({comp: SayNameComponent, inputs: {name: value}})
  }

  addNewAge(value: number) {
    this.add({comp: SayAgeComponent, inputs: {age: value}})
  }

  add(comp: any) {
    const compFactory = this.factory.componentFactories.find(x => x.componentType === comp.comp);
    // If we don't want to hold a reference to the component type, we can also say: const compFactory = this.factory.componentFactories.find(x => x.selector === 'my-component-selector');
    const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
    const cmpRef = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
    Object.keys(comp.inputs).forEach(i => cmpRef.instance[i] = comp.inputs[i]);
  }
}

@Component({
  selector: 'app-age',
  template: '<div>My age is {{age}}!</div>'
})
class SayAgeComponent {
  @Input() public age: number;
};

@Component({
  selector: 'app-name',
  template: '<div>My name is {{name}}!</div>'
})
class SayNameComponent {
  @Input() public name: string;
};

@NgModule({
  imports: [BrowserModule],
  declarations: [SayAgeComponent, SayNameComponent]
})
class DynamicModule {}

@Component({
  selector: 'app-root',
  template: `
        <h3>{{message}}</h3>
        <app-dynamic #ad></app-dynamic>
        <br>
        <input #name type="text" placeholder="name">
        <button (click)="ad.addNewName(name.value)">Add Name</button>
        <br>
        <input #age type="number" placeholder="age">
        <button (click)="ad.addNewAge(age.value)">Add Age</button>
    `,
})
export class AppComponent {
  message = 'this is app component';
  @ViewChild(DynamicComponentRenderer) dcr;

}

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, DynamicComponentRenderer],
  bootstrap: [AppComponent]
})
export class AppModule {}`

10

角度2 rc6動的コンポーネントを実行する方法を示す簡単な例があります。

たとえば、動的なhtmlテンプレート= template1があり、動的にロードしたい場合、最初にコンポーネントにラップします

@Component({template: template1})
class DynamicComponent {}

ここではhtmlとしてのtemplate1、ng2コンポーネントが含まれている可能性があります

rc6以降では、@ NgModuleでこのコンポーネントをラップする必要があります。@NgModuleは、anglarJS 1のモジュールと同様に、ng2アプリケーションの異なる部分を分離するため、次のようになります。

@Component({
  template: template1,

})
class DynamicComponent {

}
@NgModule({
  imports: [BrowserModule,RouterModule],
  declarations: [DynamicComponent]
})
class DynamicModule { }

(ここに私の例のようにRouterModuleをインポートします。後で見ることができるように私のhtmlにいくつかのルートコンポーネントがあります)

これで、DynamicModuleを次のようにコンパイルできます。 this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

それを読み込むには、app.moudule.tsに上記を追加する必要があります。私のapp.moudle.tsを参照してください。詳細および完全な詳細については、https//github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.tsを確認して ください。およびapp.moudle.tsを

デモをご覧ください:http : //plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview


3
つまり、module1、module2、module3を宣言しました。また、別の「動的」テンプレートコンテンツが必要な場合は、moudle4(module4.ts)から定義(ファイル)フォームを作成する必要がありますよね。はいの場合、それは動的ではないようです。静的ですよね?それとも私は何かを逃していますか?
RadimKöhler2016

上記の「template1」はhtmlの文字列です。何でも入れることができ、この動的テンプレートと呼ばれます。この質問では、このように質問されています
Long Field

6

angular 7.xでは、これにangular-elementsを使用しました。

  1. @ angular-elements npm i @ angular / elements -sをインストールします

  2. アクセサリサービスを作成します。

import { Injectable, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { IStringAnyMap } from 'src/app/core/models';
import { AppUserIconComponent } from 'src/app/shared';

const COMPONENTS = {
  'user-icon': AppUserIconComponent
};

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentsService {
  constructor(private injector: Injector) {

  }

  public register(): void {
    Object.entries(COMPONENTS).forEach(([key, component]: [string, any]) => {
      const CustomElement = createCustomElement(component, { injector: this.injector });
      customElements.define(key, CustomElement);
    });
  }

  public create(tagName: string, data: IStringAnyMap = {}): HTMLElement {
    const customEl = document.createElement(tagName);

    Object.entries(data).forEach(([key, value]: [string, any]) => {
      customEl[key] = value;
    });

    return customEl;
  }
}

カスタム要素タグは、角度コンポーネントセレクターとは異なる必要があることに注意してください。AppUserIconComponent:

...
selector: app-user-icon
...

この場合、カスタムタグ名は「user-icon」を使用しました。

  1. 次に、AppComponentでregisterを呼び出す必要があります。
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. そして今あなたのコードのどこでもあなたはこのようにそれを使うことができます:
dynamicComponents.create('user-icon', {user:{...}});

またはこのように:

const html = `<div class="wrapper"><user-icon class="user-icon" user='${JSON.stringify(rec.user)}'></user-icon></div>`;

this.content = this.domSanitizer.bypassSecurityTrustHtml(html);

(テンプレート内):

<div class="comment-item d-flex" [innerHTML]="content"></div>

2番目のケースでは、JSON.stringifyを使用してオブジェクトを渡し、その後再度解析する必要があることに注意してください。より良い解決策を見つけることができません。


興味深いアプローチですが、tsconfig.jsonでes2015をターゲットにする必要があるため(IE11はサポートされません)、そうでない場合は失敗しますdocument.createElement(tagName);
Snook

こんにちは、入力の処理方法について説明したように、子コンポーネントの出力もこのように処理できますか?
Mustahsan

5

ng-dynamicのdynamicComponentディレクティブを使用するだけで、Angular 2 Finalバージョンでこれを解決しました。

使用法:

<div *dynamicComponent="template; context: {text: text};"></div>

ここで、templateは動的テンプレートであり、コンテキストは、テンプレートをバインドする任意の動的データモデルに設定できます。


執筆時点では、AITを使用したAngular 5は、JITコンパイラーがバンドルに含まれていないため、これをサポートしていません。AOTなしでは魅力的に機能します:)
Richard Houltz

これはまだ角度7+に適用されますか?
Carlos E

4

Radimによるこの非常に優れた投稿の上に、いくつかの詳細を追加したいと思います。

私はこの解決策をとり、少しの間取り組んだところ、すぐにいくつかの制限にぶつかりました。私はそれらの概要を説明し、それに対する解決策も提供します。

  • 最初に、私は動的詳細の内部に動的詳細をレンダリングすることができませんでした(基本的に、動的UIを相互にネストします)。
  • 次の問題は、ソリューションで利用可能にされたパーツの1つの内部に動的な詳細をレンダリングすることでした。それも最初の解決策では不可能でした。
  • 最後に、文字列エディタのような動的な部分でテンプレートURLを使用することはできませんでした。

この投稿に基づいて、これらの制限を達成する方法について別の質問をしました。

angular2での再帰的な動的テンプレートのコンパイル

私と同じ問題が発生した場合に備えて、これらの制限に対する回答の概要を説明します。これにより、ソリューションが非常に柔軟になります。それで初期のプランカーも更新するのは素晴らしいことです。

相互に動的詳細のネストを有効にするには、type.builder.tsのインポートステートメントにDynamicModule.forRoot()を追加する必要があります。

protected createComponentModule (componentType: any) {
    @NgModule({
    imports: [
        PartsModule, 
        DynamicModule.forRoot() //this line here
    ],
    declarations: [
        componentType
    ],
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
}

その上、<dynamic-detail>文字列エディタまたはテキストエディタであるパー​​ツの1つの内部で使用することはできませんでした。

あなたが変更する必要がありますことを可能にするためparts.module.tsと、dynamic.module.ts

内部parts.module.tsあなたは追加する必要がありますDynamicDetailDYNAMIC_DIRECTIVES

export const DYNAMIC_DIRECTIVES = [
   forwardRef(() => StringEditor),
   forwardRef(() => TextEditor),
   DynamicDetail
];

また、dynamic.module.tsこれらはパーツの一部なので、dynamicDetailを削除する必要があります。

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

動作する変更されたプランカーは、http://plnkr.co/edit/UYnQHF?p = previewにあります。(私はこの問題を解決しませんでした、私は単なるメッセンジャーです:-D)

最後に、動的コンポーネントで作成されたパーツでtemplateurlsを使用することができませんでした。解決策(または回避策。角度のバグなのか、フレームワークの使い方が間違っているのかはわかりません)は、コンストラクタを挿入するのではなく、コンストラクタにコンパイラを作成することでした。

    private _compiler;

    constructor(protected compiler: RuntimeCompiler) {
        const compilerFactory : CompilerFactory =
        platformBrowserDynamic().injector.get(CompilerFactory);
        this._compiler = compilerFactory.createCompiler([]);
    }

次に、_compilerを使用してコンパイルすると、templateUrlsも有効になります。

return new Promise((resolve) => {
        this._compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                let _ = window["_"];
                factory = _.find(moduleWithFactories.componentFactories, { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

これが他の誰かを助けることを願っています!

よろしくモートン


4

Radminの優れた回答に続き、angular-cliバージョン1.0.0-beta.22以降を使用しているすべての人に必要な調整が少しあります。

COMPILER_PROVIDERSインポートできなくなりました(詳細については、angular-cli GitHubを参照してください)。

そこに使用しないことです回避策はそうCOMPILER_PROVIDERSJitCompilerしてproviders、すべての部分が、使用JitCompilerFactory代わり型ビルダークラス内でこのような「@角度/コンパイラ」から:

private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

ご覧のとおり、これは注入可能ではないため、DIとの依存関係はありません。このソリューションは、angular-cliを使用していないプロジェクトでも機能します。


1
この提案に感謝しますが、「「DynamicHtmlModule」のNgModuleメタデータが見つかりません」に遭遇しています。私の実装は、stackoverflow.com
questions / 40060498 /…に

2
誰かがAOTサンプルでJitCompiletFactoryを作業していますか?私は@Cybeyと同じエラーを持っている
user2771738


2

私自身、RC4をRC5に更新する方法を確認しようとしているので、このエントリに出くわしました。動的コンポーネント作成への新しいアプローチはまだ少し不思議なので、コンポーネントファクトリリゾルバーについては何も提案しません。

しかし、私が提案できるのは、このシナリオでコンポーネントを作成するための少し明確なアプローチです-次のような条件に従って文字列エディターまたはテキストエディターを作成するテンプレートでスイッチを使用するだけです。

<form [ngSwitch]="useTextarea">
    <string-editor *ngSwitchCase="false" propertyName="'code'" 
                 [entity]="entity"></string-editor>
    <text-editor *ngSwitchCase="true" propertyName="'code'" 
                 [entity]="entity"></text-editor>
</form>

ちなみに、[prop]式の「[」には意味があり、これは一方向のデータバインディングであることを示しています。したがって、プロパティを変数にバインドする必要がないことがわかっている場合は、これらを省略できます。


1
switch/ caseいくつかの決定が含まれている場合、それは行く方法です。しかし、生成されたテンプレートが非常に大きくなる可能性があることを想像してみてください。また、エンティティごと、セキュリティごと、エンティティステータスごと、プロパティの種類(数値、日付、参照など)ごとに異なります...そのような場合、これをhtmlテンプレートで解決するとngSwitch、非常に非常に大きなhtmlファイルが作成されます。
RadimKöhler16年

ああ、同意します。表示する特定のクラスをコンパイルする前に知らなくても、アプリケーションの主要コンポーネントをロードしようとしているので、ここにこの種のシナリオがあります。この特定のケースでは、動的コンポーネントの作成は必要ありません。
zii

1

これは、サーバーから生成された動的なフォームコントロールの例です。

https://stackblitz.com/edit/angular-t3mmg6

この例は、動的フォームコントロールが追加コンポーネント内にあります(これは、サーバーからFormcontrolを取得できる場所です)。addcomponentメソッドが表示される場合は、フォームコントロールを確認できます。この例では、角張った素材を使用していませんが、動作します(@作業を使用しています)。これは角度6のターゲットですが、以前のすべてのバージョンで機能します。

AngularVersion 5以降のJITComplierFactoryを追加する必要があります。

ありがとう

ビジェイ


0

この特定のケースでは、コンポーネントを動的に作成するためにディレクティブを使用するように見えますが、より良いオプションです。例:

コンポーネントを作成するHTML内

<ng-container dynamicComponentDirective [someConfig]="someConfig"></ng-container>

私は次の方法で指令に取り組み、設計します。

const components: {[type: string]: Type<YourConfig>} = {
    text : TextEditorComponent,
    numeric: NumericComponent,
    string: StringEditorComponent,
    date: DateComponent,
    ........
    .........
};

@Directive({
    selector: '[dynamicComponentDirective]'
})
export class DynamicComponentDirective implements YourConfig, OnChanges, OnInit {
    @Input() yourConfig: Define your config here //;
    component: ComponentRef<YourConfig>;

    constructor(
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef
    ) {}

    ngOnChanges() {
        if (this.component) {
            this.component.instance.config = this.config;
            // config is your config, what evermeta data you want to pass to the component created.
        }
    }

    ngOnInit() {
        if (!components[this.config.type]) {
            const supportedTypes = Object.keys(components).join(', ');
            console.error(`Trying to use an unsupported type ${this.config.type} Supported types: ${supportedTypes}`);
        }

        const component = this.resolver.resolveComponentFactory<yourConfig>(components[this.config.type]);
        this.component = this.container.createComponent(component);
        this.component.instance.config = this.config;
    }
}

したがって、コンポーネント内のテキスト、文字列、日付など、ng-container要素のHTMLで渡した設定はすべて利用可能です。

構成yourConfigは、同じにすることができ、メタデータを定義します。

設定または入力タイプに応じて、ディレクティブはそれに応じて動作し、サポートされているタイプから、適切なコンポーネントをレンダリングします。そうでない場合は、エラーがログに記録されます。


-1

Ophir Sternの回答の上に構築した、Angular 4のAoTで動作するバリアントは次のとおりです。唯一の問題は、DynamicComponentにサービスを挿入できないことですが、それでも問題ありません。

注:私はAngular 5でテストしていません。

import { Component, OnInit, Input, NgModule, NgModuleFactory, Compiler, EventEmitter, Output } from '@angular/core';
import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

type Bindings = {
  [key: string]: any;
};

@Component({
  selector: 'app-compile',
  template: `
    <div *ngIf="dynamicComponent && dynamicModule">
      <ng-container *ngComponentOutlet="dynamicComponent; ngModuleFactory: dynamicModule;">
      </ng-container>
    </div>
  `,
  styleUrls: ['./compile.component.scss'],
  providers: [{provide: Compiler, useFactory: createJitCompiler}]
})
export class CompileComponent implements OnInit {

  public dynamicComponent: any;
  public dynamicModule: NgModuleFactory<any>;

  @Input()
  public bindings: Bindings = {};
  @Input()
  public template: string = '';

  constructor(private compiler: Compiler) { }

  public ngOnInit() {

    try {
      this.loadDynamicContent();
    } catch (err) {
      console.log('Error during template parsing: ', err);
    }

  }

  private loadDynamicContent(): void {

    this.dynamicComponent = this.createNewComponent(this.template, this.bindings);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));

  }

  private createComponentModule(componentType: any): any {

    const runtimeComponentModule = NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })(class RuntimeComponentModule { });

    return runtimeComponentModule;

  }

  private createNewComponent(template: string, bindings: Bindings): any {

    const dynamicComponent = Component({
      selector: 'app-dynamic-component',
      template: template
    })(class DynamicComponent implements OnInit {

      public bindings: Bindings;

      constructor() { }

      public ngOnInit() {
        this.bindings = bindings;
      }

    });

    return dynamicComponent;

  }

}

お役に立てれば。

乾杯!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.