Wie kann ich eine dynamische Vorlage verwenden / erstellen, um eine dynamische Komponente mit Angular 2.0 zu kompilieren?


196

Ich möchte dynamisch eine Vorlage erstellen. Dies sollte verwendet werden, um eine ComponentTypezur Laufzeit zu erstellen und sie irgendwo innerhalb der Hosting-Komponente zu platzieren (sogar zu ersetzen) .

Bis ich RC4 verwendet habe ComponentResolver, aber mit RC5 erhalte ich die folgende Meldung:

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.

Ich habe dieses Dokument gefunden ( Angular 2 Synchronous Dynamic Component Creation )

Und verstehe, dass ich beides verwenden kann

  • Art von Dynamik ngIfmit ComponentFactoryResolver. Wenn ich bekannte Komponenten innerhalb von übergebe @Component({entryComponents: [comp1, comp2], ...})- kann ich verwenden.resolveComponentFactory(componentToRender);
  • Echte Laufzeitkompilierung mit Compiler...

Aber die Frage ist, wie man das benutzt Compiler? Der obige Hinweis besagt, dass ich anrufen sollte: Compiler.compileComponentSync/Async- wie?

Beispielsweise. Ich möchte (basierend auf einigen Konfigurationsbedingungen) diese Art von Vorlage für eine Art von Einstellungen erstellen

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

und in einem anderen Fall dieser ( string-editorwird ersetzt durch text-editor)

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

Und so weiter (unterschiedliche Anzahl / Datum / Referenz editorsnach Eigenschaftstypen, einige Eigenschaften für einige Benutzer übersprungen ...) . Dies ist ein Beispiel, die reale Konfiguration könnte viel unterschiedlichere und komplexere Vorlagen erzeugen.

Die Vorlage ändert sich, daher kann ich vorhandene nicht verwenden ComponentFactoryResolverund übergeben ... Ich benötige eine Lösung mit der Compiler.


Da die Lösung, die ich gefunden habe, so nett war, möchte ich, dass jeder, der diese Frage findet, meine Antwort sieht, die im Moment ganz unten ist. :)
Richard Houltz


Hier ist das Problem mit jeder einzelnen Antwort und was $compilediese Methoden tatsächlich nicht können - ich erstelle eine Anwendung, in der ich nur den HTML-Code kompilieren möchte, der über die Seite eines Drittanbieters und Ajax-Aufrufe eingeht. Ich kann den HTML-Code nicht von der Seite entfernen und in meine eigene Vorlage einfügen. Seufz
Augie Gardner

@AugieGardner Es gibt einen Grund, warum dies nicht beabsichtigt ist. Angular ist nicht schuld an schlechten Architekturentscheidungen oder Legacy-Systemen, die manche Leute haben. Wenn Sie vorhandenen HTML-Code analysieren möchten, können Sie ein anderes Framework verwenden, da Angular mit WebComponents einwandfrei funktioniert. Es ist wichtiger, klare Grenzen zu setzen, um die Horden unerfahrener Programmierer anzuleiten, als schmutzige Hacks für wenige Legacy-Systeme zuzulassen.
Phil

Antworten:


162

BEARBEITEN - bezogen auf 2.3.0 (07.12.2016)

HINWEIS: Um eine Lösung für die vorherige Version zu erhalten, überprüfen Sie den Verlauf dieses Beitrags

Ein ähnliches Thema wird hier behandelt. Entspricht $ compile in Angular 2 . Wir müssen JitCompilerund verwenden NgModule. Lesen NgModuleSie hier mehr über Angular2:

In einer Nussschale

Es gibt einen funktionierenden Plunker / ein funktionierendes Beispiel (dynamische Vorlage, dynamischer Komponententyp, dynamisches Modul,JitCompiler , ... in Aktion)

Das Prinzip ist:
1) erstellen Template
2) findet ComponentFactoryim Cache - gehen zu 7)
3) - erstellen Component
4) - erstellen Module
5) - Kompilierung Module
6) - Rückkehr (und Cache für eine spätere Verwendung) ComponentFactory
7) Verwendung Ziel undComponentFactory eine Instanz zu erstellen von dynamischComponent

Hier ist ein Code-Snippet (mehr davon hier ) - Unser benutzerdefinierter Builder gibt gerade erstellt / zwischengespeichert zurück ComponentFactoryund die Ansicht Zielplatzhalter verbrauchen, um eine Instanz von zu erstellenDynamicComponent

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

Das ist es - kurz gesagt. Um mehr Details zu erhalten, lesen Sie unten

.

TL & DR

Beobachten Sie einen Plunker und lesen Sie die Details, falls ein Ausschnitt weitere Erklärungen erfordert

.

Detaillierte Erklärung - Angular2 RC6 ++ & Laufzeitkomponenten

Nachfolgend werden wir dieses Szenario beschreiben

  1. Erstellen Sie ein Modul PartsModule:NgModule (Halter von kleinen Stücken)
  2. Erstellen Sie ein weiteres Modul DynamicModule:NgModule, das unsere dynamische Komponente enthält (und PartsModuledynamisch referenziert ).
  3. dynamische Vorlage erstellen (einfacher Ansatz)
  4. neuen ComponentTyp erstellen (nur wenn sich die Vorlage geändert hat)
  5. neu erstellen RuntimeModule:NgModule. Dieses Modul enthält den zuvor erstellten ComponentTyp
  6. Anruf JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)zu bekommenComponentFactory
  7. Erstellen Sie eine Instanz des DynamicComponentJobs - des Platzhalters "Ziel anzeigen" undComponentFactory
  8. zuweisen , @Inputsum neue Instanz (Schalter aus INPUTzu TEXTAREAbearbeiten) , verbrauchen@Outputs

NgModule

Wir brauchen ein NgModules.

Während ich ein sehr einfaches Beispiel zeigen möchte, würde ich in diesem Fall drei Module benötigen (tatsächlich 4 - aber ich zähle das AppModule nicht) . Bitte nehmen Sie dies anstelle eines einfachen Ausschnitts als Grundlage für einen wirklich soliden Generator für dynamische Komponenten.

Es wird ein Modul für alle Kleinteile, zum Beispiel string-editor, text-editor ( date-editor, number-editor...)

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

Wo DYNAMIC_DIRECTIVESsind erweiterbar und sollen alle Kleinteile aufnehmen, die für unsere dynamische Komponentenvorlage / unseren dynamischen Komponententyp verwendet werden. Überprüfen Sie app / parts / parts.module.ts

Das zweite wird ein Modul für unser dynamisches Handling sein. Es wird Hosting-Komponenten und einige Anbieter enthalten. Dies werden Singletons sein. Dafür werden wir sie standardmäßig veröffentlichen - mitforRoot()

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
            ], 
        };
    }
}

Überprüfen Sie die Verwendung der forRoot()in derAppModule

Schließlich benötigen wir ein Ad-hoc-Laufzeitmodul. Dieses wird jedoch später als Teil des DynamicTypeBuilderJobs erstellt.

Das vierte Modul, das Anwendungsmodul, ist dasjenige, das Compiler-Anbieter deklariert:

...
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
  ],

Lesen Sie viel mehr über NgModule es:

EIN Template Builder

In unserem Beispiel werden wir Details dieser Art von Entität verarbeiten

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

Um ein zu erstellen template, verwenden wir in diesem Plunker diesen einfachen / naiven Builder.

Die eigentliche Lösung, ein echter Vorlagenersteller, ist der Ort, an dem Ihre Anwendung viel leisten kann

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

Ein Trick dabei ist - es erstellt eine Vorlage, die eine Reihe bekannter Eigenschaften verwendet, z entity . Solche Eigenschaften müssen Teil einer dynamischen Komponente sein, die wir als nächstes erstellen werden.

Um es ein bisschen einfacher zu machen, können wir eine Schnittstelle verwenden, um Eigenschaften zu definieren, die unser Template Builder verwenden kann. Dies wird durch unseren dynamischen Komponententyp implementiert.

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

Ein ComponentFactoryBaumeister

Sehr wichtig ist hierbei Folgendes zu beachten:

Unser Komponententyp, der mit unserem erstellt wurde DynamicTypeBuilder, kann sich unterscheiden - jedoch nur durch die Vorlage (oben erstellt) . Die Eigenschaften der Komponenten (Eingänge, Ausgänge oder einige geschützte) sind immer noch dieselben. Wenn wir unterschiedliche Eigenschaften benötigen, sollten wir eine unterschiedliche Kombination aus Template und Type Builder definieren

Wir berühren also den Kern unserer Lösung. Der Builder wird 1) erstellen ComponentType2) erstellen NgModule3) kompilieren ComponentFactory4) zwischenspeichern für die spätere Wiederverwendung.

Eine Abhängigkeit, die wir erhalten müssen:

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

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

Und hier ist ein Ausschnitt, wie man ein 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);
            });
    });
}

Oben erstellen und zwischenspeichern wir beide Componentund Module. Denn wenn die Vorlage (in der Tat der eigentliche dynamische Teil von allem) dieselbe ist, können wir sie wiederverwenden

Und hier sind zwei Methoden, die die wirklich coole Art darstellen, dekorierte Klassen / Typen zur Laufzeit zu erstellen . Nicht nur, @Componentsondern auch die@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;
}

Wichtig:

Unsere dynamischen Komponententypen unterscheiden sich, jedoch nur nach Vorlage. Wir nutzen diese Tatsache, um sie zwischenzuspeichern. Das ist wirklich sehr wichtig. Angular2 speichert diese auch nach Typ . Und wenn wir für die gleichen Vorlagenzeichenfolgen neue Typen neu erstellen würden ... werden wir anfangen, Speicherlecks zu generieren.

ComponentFactory wird von der Hosting-Komponente verwendet

Das letzte Stück ist eine Komponente, die das Ziel für unsere dynamische Komponente enthält, z <div #dynamicContentPlaceHolder></div>. Wir erhalten einen Verweis darauf und ComponentFactoryerstellen damit eine Komponente. Das ist auf den Punkt gebracht, und hier sind alle Teile dieser Komponente (falls erforderlich, öffnen Sie hier den Plunker ).

Fassen wir zunächst die Importanweisungen zusammen:

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
    ) {}
    ...

Wir erhalten nur Builder für Vorlagen und Komponenten. Als nächstes folgen die Eigenschaften, die für unser Beispiel benötigt werden (mehr in den Kommentaren).

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

In diesem einfachen Szenario hat unsere Hosting-Komponente keine @Input. Es muss also nicht auf Änderungen reagieren. Aber trotz dieser Tatsache (und um auf kommende Änderungen vorbereitet zu sein) - müssen wir ein Flag einführen, wenn die Komponente bereits war (erstens) initiiert wurde. Und nur dann können wir die Magie beginnen.

Schließlich werden wir unseren Komponenten-Builder verwenden, der gerade kompiliert / zwischengespeichert wurde ComponentFacotry . Unser Zielplatzhalter wird gebeten, dasComponent mit dieser Fabrik zu instanziieren .

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

kleine Erweiterung

Außerdem müssen wir einen Verweis auf die kompilierte Vorlage behalten, um sie ordnungsgemäß zu verwenden destroy(), wann immer wir sie ändern werden.

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

getan

Das ist so ziemlich alles. Vergessen Sie nicht, alles zu zerstören , was dynamisch erstellt wurde (ngOnDestroy) . Auch sicher sein , zu Cache dynamisch typesundmodules wenn der einzige Unterschied ist ihre Vorlage.

Überprüfen Sie sie alle in Aktion hier

Überprüfen Sie den Verlauf , um frühere Versionen (z. B. RC5) dieses Beitrags anzuzeigen


50
das sieht nach einer so komplizierten Lösung aus, die veraltete war sehr einfach und klar, gibt es eine andere Möglichkeit, dies zu tun?
Tibbus

3
Ich denke genauso wie @tibbus: Das wurde viel komplizierter als früher mit dem veralteten Code. Vielen Dank für Ihre Antwort.
Lucio Mollinedo

5
@ Ribsies danke für deinen Hinweis. Lassen Sie mich etwas klarstellen. Viele andere Antworten versuchen es einfach zu machen . Aber ich versuche es zu erklären und in einem Szenario zu zeigen, das für den realen Gebrauch geschlossen ist . Wir müssten Sachen zwischenspeichern, wir müssten bei der Neuerstellung "Zerstören" aufrufen usw. Also, während die Magie des dynamischen Bauens wirklich so ist, type.builder.tswie Sie es gezeigt haben, wünschte ich mir, dass jeder Benutzer verstehen würde, wie man das alles einfügt Kontext ... Hoffe, es könnte nützlich sein;)
Radim Köhler

7
@ Radim Köhler - Ich habe dieses Beispiel ausprobiert. es funktioniert ohne AOT. Aber als ich versucht habe, dies mit AOT auszuführen, wird der Fehler "Keine NgModule-Metadaten für RuntimeComponentModule gefunden" angezeigt. Können Sie mir bitte helfen, diesen Fehler zu beheben?
Trusha

4
Die Antwort selbst ist perfekt! Aber für reale Anwendungen nicht praktikabel. Das Angular-Team sollte hierfür eine Lösung im Framework bereitstellen, da dies in Geschäftsanwendungen häufig erforderlich ist. Wenn nicht, muss gefragt werden, ob Angular 2 die richtige Plattform für Geschäftsanwendungen ist.
Karl

58

BEARBEITEN (26.08.2017) : Die folgende Lösung funktioniert gut mit Angular2 und 4. Ich habe sie aktualisiert, um eine Vorlagenvariable und einen Klick-Handler zu enthalten, und sie mit Angular 4.3 getestet.
Für Angular4 ist ngComponentOutlet, wie in Ophirs Antwort beschrieben, eine viel bessere Lösung. Derzeit werden jedoch keine Ein- und Ausgänge unterstützt . Wenn [diese PR] ( https://github.com/angular/angular/pull/15362] akzeptiert wird, ist dies über die vom create-Ereignis zurückgegebene Komponenteninstanz möglich.
Ng-dynamic-component möglicherweise die beste und einfachste Lösung insgesamt, aber ich habe das noch nicht getestet.

Die Antwort von @Long Field ist genau richtig! Hier ist ein weiteres (synchrones) Beispiel:

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 {}

Live unter http://plnkr.co/edit/fdP9Oc .


3
Ich würde sagen, dass dies ein Beispiel dafür ist, wie man so wenig Code wie möglich schreibt , um dasselbe zu tun wie in meiner Antwort stackoverflow.com/a/38888009/1679310 . Für den Fall, dass es ein nützlicher Fall sein sollte (meistens eine RE-generierende Vorlage), wenn sich die Bedingung ändert ... der einfache ngAfterViewInitAufruf mit einem const templatefunktioniert nicht. Aber wenn Ihre Aufgabe darin bestand, den oben beschriebenen Ansatz zu reduzieren (Vorlage erstellen, Komponente erstellen, Modul erstellen, kompilieren, Factory erstellen ... Instanz erstellen) ... haben Sie es wahrscheinlich getan
Radim Köhler

Vielen Dank für die Lösung: Ich habe jedoch Probleme beim Laden von templateUrl und Styles. Es wird folgende Fehlermeldung angezeigt: Es wurde keine ResourceLoader-Implementierung bereitgestellt. Kann die URL localhost nicht lesen : 3000 / app / pages / pages_common.css , eine Idee, was mir fehlt?
Gerardlamo

Könnte es möglich sein, die HTML-Vorlage mit Daten zu kompilieren, die für Zellen in gitterähnlichen Steuerelementen spezifisch sind? plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview Wie kann ich in diesem Plunker das Bild kompilieren und in der letzten Spalte anzeigen? Irgendeine Hilfe.?
Karthick

1
@ Monnef, du hast recht. Ich habe das Konsolenprotokoll nicht überprüft. Ich habe den Code so angepasst, dass die Komponente im ngOnInit-Hook und nicht im ngAfterViewInit-Hook hinzugefügt wird, da ersterer vor und letzterer nach der Änderungserkennung ausgelöst wird . (Siehe github.com/angular/angular/issues/10131 und ähnliche Themen.)
Rene Hamburger

1
ordentlich und einfach. Funktionierte wie erwartet beim Servieren über den Browser in dev. Aber funktioniert das mit AOT? Wenn die App nach der Kompilierung in PROD ausgeführt wird, wird beim Versuch der Komponentenkompilierung die Meldung "Fehler: Laufzeit-Compiler wird nicht geladen" angezeigt. (Übrigens benutze ich Ionic 3.5)
mymo

52

Ich muss spät auf der Party angekommen sein, keine der Lösungen hier schien mir hilfreich zu sein - zu chaotisch und fühlte sich wie eine zu große Problemumgehung an.

Was ich am Ende ist zu tun mit Angular 4.0.0-beta.6‚s ngComponentOutlet .

Dies gab mir die kürzeste und einfachste Lösung, die alle in die Datei der dynamischen Komponente geschrieben wurden.

  • Hier ist ein einfaches Beispiel, das nur Text empfängt und in eine Vorlage einfügt, aber natürlich können Sie ihn nach Bedarf ändern:
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;
  }
}
  • Kurze Erklärung:
    1. my-component - die Komponente, in der eine dynamische Komponente gerendert wird
    2. DynamicComponent - Die Komponente, die dynamisch erstellt werden soll und in meiner Komponente gerendert wird

Vergessen Sie nicht, alle Winkelbibliotheken auf ^ Angular 4.0.0 zu aktualisieren

Hoffe das hilft, viel Glück!

AKTUALISIEREN

Funktioniert auch für Winkel 5.


3
Das hat bei Angular4 sehr gut funktioniert. Die einzige Anpassung, die ich vornehmen musste, bestand darin, Importmodule für das dynamisch erstellte RuntimeComponentModule angeben zu können.
Rahul Patel

8
Hier ist ein kurzes Beispiel ausgehend vom Angular Quickstart
Rahul Patel

5
Funktioniert diese Lösung mit "ng build --prod"? Es scheint, dass die Compiler-Klasse und AoT atm nicht zusammenpassen.
Pierre Chavaroche

2
@OphirStern Ich habe auch festgestellt, dass dieser Ansatz in Angular 5 gut funktioniert, aber NICHT mit dem Build-Flag --prod.
TaeKwonJoe

2
Ich habe es mit Winkel 5 (5.2.8) mit der JitCompilerFactory getestet und mit dem Flag --prod funktioniert es nicht! Hat jemand eine Lösung? (BTW JitCompilerFactory ohne das --prod Flag funktioniert einwandfrei)
Frank

20

2019 Juni Antwort

Großartige Neuigkeiten! Es scheint, dass das @ angle / cdk- Paket jetzt erstklassige Unterstützung für Portale bietet !

Zum Zeitpunkt des Schreibens fand ich die oben genannten offiziellen Dokumente nicht besonders hilfreich (insbesondere im Hinblick auf das Senden von Daten und das Empfangen von Ereignissen von den dynamischen Komponenten). Zusammenfassend müssen Sie:

Schritt 1) ​​Aktualisieren Sie Ihre AppModule

Importieren Sie PortalModuleaus dem @angular/cdk/portalPaket und registrieren Sie Ihre dynamischen Komponenten darinentryComponents

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

Schritt 2. Option A: Wenn Sie KEINE Daten an Ihre dynamischen Komponenten übergeben und Ereignisse von diesen empfangen müssen :

@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{
}

Sehen Sie es in Aktion

Schritt 2. Option B: Wenn Sie Daten an Ihre dynamischen Komponenten übergeben und Ereignisse von diesen empfangen müssen :

// 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 = '';
}

Sehen Sie es in Aktion


1
Alter, du hast es gerade geschafft. Dieser wird Aufmerksamkeit bekommen. Ich konnte nicht glauben, wie verdammt schwierig es ist, eine einfache dynamische Komponente in Angular hinzuzufügen, bis ich eine machen musste. Es ist wie beim Zurücksetzen und Zurückkehren zu den Zeiten vor JQuery.
Gi1ber7

2
@ Gi1ber7 Ich weiß richtig? Warum haben sie so lange gebraucht?
Stephen Paul

1
Netter Ansatz, aber wissen Sie, wie man Parameter an ChildComponent übergibt?
Snook

1
@ Snook dies kann Ihre Frage beantworten stackoverflow.com/questions/47469844/…
Stephen Paul

4
@StephenPaul Wie unterscheidet sich dieser PortalAnsatz von ngTemplateOutletund ngComponentOutlet? 🤔
Glenn Mohammad

18

Ich beschloss, alles, was ich gelernt hatte, in eine Datei zu komprimieren . Hier gibt es besonders im Vergleich zu vor RC5 viel zu beachten. Beachten Sie, dass diese Quelldatei AppModule und AppComponent enthält.

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

Ich habe ein einfaches Beispiel, um zu zeigen, wie man eine dynamische 2-rc6-Komponente mit Winkel erstellt.

Angenommen, Sie haben eine dynamische HTML-Vorlage = template1 und möchten dynamisch laden, indem Sie zunächst eine Komponente einbinden

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

hier kann template1 als html die ng2-komponente enthalten

Ab rc6 muss @NgModule diese Komponente umbrechen. @NgModule, genau wie das Modul in anglarJS 1, entkoppelt es verschiedene Teile der ng2-Anwendung, also:

@Component({
  template: template1,

})
class DynamicComponent {

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

(Importieren Sie hier RouterModule, da in meinem Beispiel einige Routenkomponenten in meinem HTML-Code enthalten sind, wie Sie später sehen können.)

Jetzt können Sie DynamicModule wie folgt kompilieren: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

Und wir müssen oben in app.moudule.ts setzen, um es zu laden, siehe meine app.moudle.ts. Weitere und ausführlichere Informationen finden Sie unter: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts und app.moudle.ts

und siehe Demo: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview


3
Sie haben also Modul1, Modul2, Modul3 deklariert. Und wenn Sie einen anderen "dynamischen" Vorlageninhalt benötigen würden, müssten Sie eine Definition (Datei) von moudle4 (module4.ts) erstellen, oder? Wenn ja, scheint das nicht dynamisch zu sein. Es ist statisch, nicht wahr? Oder vermisse ich etwas?
Radim Köhler

Oben ist "template1" eine HTML-Zeichenfolge, Sie können alles darin einfügen und wir nennen diese dynamische Vorlage, da diese Frage gestellt wird
Long Field

6

In Winkel 7.x habe ich dafür Winkelelemente verwendet.

  1. Installieren Sie @ angle-elements npm i @ angle / elements -s

  2. Zubehörservice erstellen.

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

Beachten Sie, dass Ihr benutzerdefiniertes Element-Tag mit dem Winkelkomponenten-Selektor unterschiedlich sein muss. in AppUserIconComponent:

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

und in diesem Fall habe ich "Benutzer-Symbol" verwendet.

  1. Dann müssen Sie register in AppComponent aufrufen:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. Und jetzt können Sie es an jeder Stelle Ihres Codes folgendermaßen verwenden:
dynamicComponents.create('user-icon', {user:{...}});

oder so:

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

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

(in Vorlage):

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

Beachten Sie, dass Sie im zweiten Fall Objekte mit JSON.stringify übergeben und anschließend erneut analysieren müssen. Ich kann keine bessere Lösung finden.


Interessanter Ansatz, aber Sie müssen es2015 (also keine Unterstützung für IE11) in Ihrer tsconfig.json als Ziel festlegen, andernfalls wird es umdocument.createElement(tagName);
Snook

Hallo, wie Sie bereits erwähnt haben, können Eingaben von untergeordneten Komponenten auch so behandelt werden?
Mustahsan

5

Dies wurde in der endgültigen Version von Angular 2 einfach mithilfe der Direktive dynamicComponent von ng-dynamic behoben .

Verwendung:

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

Wo Vorlage Ihre dynamische Vorlage ist und der Kontext auf ein beliebiges dynamisches Datenmodell festgelegt werden kann, an das Ihre Vorlage gebunden werden soll.


Zum Zeitpunkt des Schreibens unterstützt Angular 5 mit AOT dies nicht, da der JIT-Compiler nicht im Bundle enthalten ist. Ohne AOT funktioniert es wie ein Zauber :)
Richard Houltz

Gilt das immer noch für Winkel 7+?
Carlos E

4

Ich möchte ein paar Details zu diesem sehr ausgezeichneten Beitrag von Radim hinzufügen.

Ich nahm diese Lösung und arbeitete ein wenig daran und stieß schnell auf einige Einschränkungen. Ich werde diese nur skizzieren und dann auch die Lösung dafür geben.

  • Erstens war ich nicht in der Lage, dynamische Details in dynamischen Details zu rendern (im Grunde genommen dynamische Benutzeroberflächen ineinander verschachteln).
  • Das nächste Problem war, dass ich ein dynamisches Detail in einem der Teile rendern wollte, die in der Lösung verfügbar gemacht wurden. Das war auch mit der Ausgangslösung nicht möglich.
  • Schließlich war es nicht möglich, Vorlagen-URLs für dynamische Teile wie den String-Editor zu verwenden.

Auf der Grundlage dieses Beitrags habe ich eine weitere Frage gestellt, wie diese Einschränkungen erreicht werden können. Diese finden Sie hier:

rekursive dynamische Vorlagenkompilierung in angle2

Ich werde nur die Antworten auf diese Einschränkungen skizzieren, falls Sie auf dasselbe Problem wie ich stoßen, da dies die Lösung sehr flexibler macht. Es wäre fantastisch, wenn auch der erste Plunker damit aktualisiert würde.

Um das Verschachteln von dynamischen Details ineinander zu aktivieren, müssen Sie DynamicModule.forRoot () in der import-Anweisung in type.builder.ts hinzufügen

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

Außerdem war es nicht möglich zu verwenden <dynamic-detail> innerhalb eines der Teile String-Editor oder Text-Editor zu verwenden.

Um dies zu aktivieren, müssen Sie parts.module.tsund änderndynamic.module.ts

Innerhalb parts.module.tsSie müssen hinzufügen DynamicDetailin derDYNAMIC_DIRECTIVES

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

Außerdem dynamic.module.tsmüssten Sie das dynamicDetail entfernen, da sie jetzt Teil der Teile sind

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

Ein funktionierender modifizierter Plunker finden Sie hier: http://plnkr.co/edit/UYnQHF?p=preview (Ich habe dieses Problem nicht gelöst, ich bin nur der Messenger :-D)

Schließlich war es nicht möglich, Templateurls in den Teilen zu verwenden, die auf den dynamischen Komponenten erstellt wurden. Eine Lösung (oder Problemumgehung. Ich bin nicht sicher, ob es sich um einen Winkelfehler oder eine falsche Verwendung des Frameworks handelt) bestand darin, einen Compiler im Konstruktor zu erstellen, anstatt ihn zu injizieren.

    private _compiler;

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

Verwenden Sie dann die _compilerzum Kompilieren, dann werden auch templateUrls aktiviert.

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

Hoffe das hilft jemand anderem!

Viele Grüße Morten


4

Nach Radmins hervorragender Antwort ist für alle, die Angular-Cli Version 1.0.0-Beta.22 und höher verwenden, eine kleine Optimierung erforderlich.

COMPILER_PROVIDERSkann nicht mehr importiert werden (Details siehe angle-cli GitHub ).

Die Problemumgehung besteht also darin, nicht COMPILER_PROVIDERSund JitCompilerim providersAbschnitt überhaupt zu verwenden, sondern JitCompilerFactoryvon '@ angle / compiler' stattdessen wie folgt innerhalb der Type Builder-Klasse zu verwenden:

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

Wie Sie sehen können, ist es nicht injizierbar und hat daher keine Abhängigkeiten vom DI. Diese Lösung sollte auch für Projekte funktionieren, die Angular-Cli nicht verwenden.


1
Vielen Dank für diesen Vorschlag. Ich stoße jedoch auf "Keine NgModule-Metadaten für 'DynamicHtmlModule' gefunden". Meine Implementierung basiert auf stackoverflow.com/questions/40060498/…
Cybey

2
Hat jemand JitCompiletFactory mit AOT-Beispiel gearbeitet? Ich habe den gleichen Fehler wie @Cybey
user2771738


2

Ich selbst versuche zu sehen, wie ich RC4 auf RC5 aktualisieren kann, und bin daher auf diesen Eintrag gestoßen, und der neue Ansatz zur dynamischen Komponentenerstellung ist mir immer noch ein Rätsel, daher schlage ich nichts zum Komponenten-Factory-Resolver vor.

Was ich jedoch vorschlagen kann, ist ein etwas klarerer Ansatz für die Erstellung von Komponenten in diesem Szenario. Verwenden Sie einfach den Schalter in der Vorlage, mit dem unter bestimmten Bedingungen wie folgt ein Zeichenfolgen- oder Texteditor erstellt wird:

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

Übrigens hat "[" im Ausdruck [prop] eine Bedeutung, dies zeigt eine Einweg-Datenbindung an. Daher können und sollten Sie diese weglassen, falls Sie wissen, dass Sie die Eigenschaft nicht an eine Variable binden müssen.


1
Das wäre ein langer Weg, wenn das switch/ casenur wenige Entscheidungen enthält. Stellen Sie sich jedoch vor, dass die generierte Vorlage sehr groß sein könnte ... und sich für jede Entität, je nach Sicherheit, je nach Entitätsstatus und jedem Eigenschaftstyp (Nummer, Datum, Referenz ... Editoren) unterscheiden könnte. Wenn Sie dies in einer HTML-Vorlage mit lösen ngSwitch, wird eine große, sehr, sehr große htmlDatei erstellt.
Radim Köhler

Oh, ich stimme dir zu. Ich habe diese Art von Szenario genau hier, gerade jetzt, da ich versuche, wichtige Komponenten der Anwendung zu laden, ohne zu wissen, dass vor dem Kompilieren eine bestimmte Klasse angezeigt werden soll. Obwohl dieser spezielle Fall keine dynamische Komponentenerstellung erfordert.
Zii

1

Dies ist das Beispiel für dynamische Formularsteuerelemente, die vom Server generiert werden.

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

In diesem Beispiel handelt es sich um dynamische Formularsteuerelemente, die in der Komponente hinzufügen enthalten sind (hier können Sie die Formularsteuerelemente vom Server abrufen). Wenn die Methode addcomponent angezeigt wird, werden die Formularsteuerelemente angezeigt. In diesem Beispiel verwende ich kein eckiges Material, aber es funktioniert (ich verwende @ work). Dies ist das Ziel von Winkel 6, funktioniert jedoch in allen vorherigen Versionen.

JITComplierFactory für AngularVersion 5 und höher muss hinzugefügt werden.

Vielen Dank

Vijay


0

In diesem speziellen Fall wäre die Verwendung einer Direktive zum dynamischen Erstellen der Komponente eine bessere Option. Beispiel:

In dem HTML, in dem Sie die Komponente erstellen möchten

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

Ich würde die Richtlinie folgendermaßen angehen und gestalten.

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

Also in Ihren Komponenten Text, Zeichenfolge, Datum, was auch immer - unabhängig von der Konfiguration, die Sie im HTML in der übergeben haben ng-container Element übergeben haben, wäre verfügbar.

Die Konfiguration, yourConfig kann identisch sein und Ihre Metadaten definieren.

Abhängig von Ihrer Konfiguration oder Ihrem Eingabetyp sollte die Direktive entsprechend handeln und von den unterstützten Typen die entsprechende Komponente rendern. Wenn nicht, wird ein Fehler protokolliert.


-1

Aufbauend auf der Antwort von Ophir Stern ist hier eine Variante, die mit AoT in Angular 4 funktioniert. Das einzige Problem, das ich habe, ist, dass ich keine Dienste in die DynamicComponent einfügen kann, aber damit kann ich leben.

Hinweis: Ich habe nicht mit Angular 5 getestet.

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;

  }

}

Hoffe das hilft.

Prost!

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.