Come posso utilizzare / creare un modello dinamico per compilare un componente dinamico con Angular 2.0?


197

Voglio creare dinamicamente un modello. Questo dovrebbe essere usato per creare un ComponentTypea runtime e posizionarlo (anche sostituirlo) da qualche parte all'interno del Componente di hosting.

Fino a RC4 che stavo usando ComponentResolver, ma con RC5 ricevo il seguente messaggio:

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.

Ho trovato questo documento ( Angular 2 Synchronous Dynamic Component Creation )

E capire che posso usare entrambi

  • Tipo di dinamica ngIfcon ComponentFactoryResolver. Se passo componenti noti all'interno di @Component({entryComponents: [comp1, comp2], ...})- Posso usare.resolveComponentFactory(componentToRender);
  • Compilazione di runtime reale, con Compiler...

Ma la domanda è come usarlo Compiler? La nota sopra dice che dovrei chiamare: Compiler.compileComponentSync/Async- allora come?

Per esempio. Voglio creare (sulla base di alcune condizioni di configurazione) questo tipo di modello per un tipo di impostazioni

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

e in un altro caso questo ( string-editorviene sostituito con text-editor)

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

E così via (numero / data / riferimento diversi editorsper tipo di proprietà, alcune proprietà saltate per alcuni utenti ...) . cioè questo è un esempio, la configurazione reale potrebbe generare modelli molto più diversi e complessi.

Il modello sta cambiando, quindi non posso usare ComponentFactoryResolvere passare quelli esistenti ... Ho bisogno di una soluzione con Compiler.


Dato che la soluzione che ho trovato è stata così carina, voglio che tutti trovino questa domanda per dare un'occhiata alla mia risposta che è molto in fondo al momento. :)
Richard Houltz,

L'articolo Ecco ciò che devi sapere sui componenti dinamici in Angular, che offre una grande spiegazione dei componenti dinamici.
Max Koretskyi,

Ecco il problema con ogni singola risposta là fuori e cosa $compilepotrebbe effettivamente fare che questi metodi non possono - sto creando un'applicazione in cui voglio solo compilare l'HTML quando arriva attraverso una pagina di terze parti e chiamate ajax. Non riesco a rimuovere l'HTML dalla pagina e inserirlo nel mio modello. Sospiro
Augie Gardner,

@AugieGardner C'è un motivo per cui questo non è possibile in base alla progettazione. Angular non è in colpa per decisioni architettoniche sbagliate o sistemi legacy che alcune persone hanno. Se vuoi analizzare il codice HTML esistente sei libero di usare un altro framework poiché Angular funziona perfettamente con WebComponents. Stabilire confini chiari per guidare le orde di programmatori inesperti è più importante che consentire hack sporchi per pochi sistemi legacy.
Phil

Risposte:


163

EDIT - relativo a 2.3.0 (2016-12-07)

NOTA: per ottenere la soluzione per la versione precedente, controlla la cronologia di questo post

Argomento simile è discusso qui Equivalente di $ compilare in Angular 2 . Dobbiamo usare JitCompilere NgModule. Maggiori informazioni su NgModulein Angular2 qui:

In breve

Esiste un plunker / esempio funzionante (modello dinamico, tipo di componente dinamico, modulo dinamico JitCompiler, ... in azione)

L'entità è:
1) crea il modello
2) trova ComponentFactorynella cache - vai a 7)
3) - crea Component
4) - crea Module
5) - compila Module
6) - restituisce (e cache per un uso successivo) ComponentFactory
7) usa Target e ComponentFactoryper creare un'istanza di dinamicoComponent

Ecco uno snippet di codice (altro qui ) - Il nostro generatore personalizzato sta restituendo appena creato / memorizzato nella cache ComponentFactorye la vista Segnaposto di destinazione consuma per creare un'istanza delDynamicComponent

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

Questo è tutto - in poche parole. Per maggiori dettagli .. leggi sotto

.

TL & DR

Osserva un plunker e torna a leggere i dettagli nel caso in cui alcuni frammenti richiedano ulteriori spiegazioni

.

Spiegazione dettagliata - Angular2 RC6 ++ e componenti di runtime

Di seguito la descrizione di questo scenario , lo faremo

  1. creare un modulo PartsModule:NgModule (supporto di piccoli pezzi)
  2. creare un altro modulo DynamicModule:NgModule, che conterrà il nostro componente dinamico (e fare riferimento PartsModuledinamicamente)
  3. creare un modello dinamico (approccio semplice)
  4. creare nuovo Component tipo (solo se il modello è cambiato)
  5. creare nuovo RuntimeModule:NgModule . Questo modulo conterrà il Componenttipo precedentemente creato
  6. chiamata JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) per ottenereComponentFactory
  7. creare un'istanza di DynamicComponent - lavoro del segnaposto Visualizza destinazione eComponentFactory
  8. assegnare @Inputsalla nuova istanza (interruttore da INPUTa TEXTAREAmodifica) , consumano@Outputs

NgModule

Abbiamo bisogno di un NgModule s.

Mentre vorrei mostrare un esempio molto semplice, in questo caso, avrei bisogno di tre moduli (in effetti 4 - ma non conto l'AppModule) . Per favore, prendi questo piuttosto che un semplice frammento come base per un generatore di componenti dinamici davvero solido.

Ci sarà un modulo per tutti i piccoli componenti, ad es string-editor. text-editor ( date-editor, number-editor...)

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

Dove DYNAMIC_DIRECTIVESsono estensibili e destinati a contenere tutte le piccole parti utilizzate per il nostro modello / tipo di componente dinamico. Controlla app / parts / parts.module.ts

Il secondo sarà il modulo per la nostra gestione dinamica delle cose. Conterrà componenti di hosting e alcuni provider .. che saranno singoli. Pertanto li pubblicheremo in modo standard - conforRoot()

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

Controllare l'uso di forRoot()inAppModule

Infine, avremo bisogno di un modulo di runtime ad hoc ... ma che verrà creato in seguito, come parte del DynamicTypeBuilderlavoro.

Il quarto modulo, modulo applicativo, è quello che continua a dichiarare i provider di compilatori:

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

Leggi (leggi) molto di più su NgModule lì:

Un modello builder

Nel nostro esempio elaboreremo i dettagli di questo tipo di entità

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

Per creare un template, in questo plunker usiamo questo builder semplice / ingenuo.

La vera soluzione, un vero generatore di modelli, è il luogo in cui l'applicazione può fare molto

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

Un trucco qui è: crea un modello che utilizza un insieme di proprietà note, ad es entity. Tale proprietà (-ies) deve far parte del componente dinamico, che creeremo successivamente.

Per renderlo un po 'più semplice, possiamo usare un'interfaccia per definire le proprietà, che il nostro template builder può usare. Questo sarà implementato dal nostro tipo di componente dinamico.

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

UN ComponentFactory costruttore

La cosa molto importante qui è da tenere a mente:

il nostro tipo di componente, costruito con il nostro DynamicTypeBuilder, potrebbe differire, ma solo dal suo modello (creato sopra) . Le proprietà dei componenti (ingressi, uscite o alcuni protetti) sono ancora le stesse. Se abbiamo bisogno di proprietà diverse, dovremmo definire diverse combinazioni di Template e Type Builder

Quindi, stiamo toccando il nucleo della nostra soluzione. The Builder, 1) creerà ComponentType2) creerà la sua NgModule3) compilazione ComponentFactory4) memorizzerà nella cache per un successivo riutilizzo.

Una dipendenza che dobbiamo ricevere:

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

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

Ed ecco uno snippet come ottenere un 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);
            });
    });
}

Sopra creiamo e memorizziamo nella cache entrambi Componente Module. Perché se il modello (in effetti la vera parte dinamica di tutto questo) è lo stesso .. possiamo riutilizzarlo

E qui ci sono due metodi, che rappresentano il modo davvero fantastico di creare classi / tipi decorati in fase di esecuzione. Non solo @Componentma anche il@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;
}

Importante:

i nostri tipi dinamici dei componenti differiscono, ma solo per modello. Quindi usiamo questo fatto per memorizzarli nella cache . Questo è davvero molto importante. Anche Angular2 li memorizzerà nella cache in base al tipo . E se dovessimo ricreare per lo stesso modello stringhe di nuovi tipi ... inizieremo a generare perdite di memoria.

ComponentFactory utilizzato dal componente di hosting

Il pezzo finale è un componente che ospita l'obiettivo del nostro componente dinamico, ad es <div #dynamicContentPlaceHolder></div>. Otteniamo un riferimento ad esso e utilizziamo ComponentFactoryper creare un componente. Questo è in breve, e qui ci sono tutti i pezzi di quel componente (se necessario, apri il plunker qui )

Riassumiamo innanzitutto le dichiarazioni di importazione:

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

Riceviamo solo costruttori di modelli e componenti. Successivamente ci sono le proprietà che sono necessarie per il nostro esempio (più nei commenti)

// 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 questo semplice scenario, il nostro componente di hosting non ne ha @Input. Quindi non deve reagire ai cambiamenti. Ma nonostante ciò (e per essere pronti per i prossimi cambiamenti) - dobbiamo introdurre alcuni flag se il componente era già (in primo luogo) avviato. E solo allora possiamo iniziare la magia.

Infine useremo il nostro componente builder, ed è appena compilato / memorizzato nella cache ComponentFacotry . Il nostro segnaposto di destinazione verrà chiesto di istanziare laComponent con quella fabbrica.

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

piccola estensione

Inoltre, dobbiamo mantenere un riferimento al modello compilato ... per poterlo correttamente destroy(), ogni volta che lo cambieremo.

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

fatto

È praticamente tutto. Non dimenticare di distruggere tutto ciò che è stato costruito in modo dinamico (ngOnDestroy) . Inoltre, assicurati di memorizzare nella cache dinamica typese modulesse l'unica differenza è il loro modello.

Controlla tutto in azione qui

per vedere le versioni precedenti (es. relative a RC5) di questo post, controlla la cronologia


50
questa sembra una soluzione così complicata, quella deprecata era molto semplice e chiara, c'è un altro modo per farlo?
tibbus,

3
Penso allo stesso modo di @tibbus: questo è diventato molto più complicato rispetto al codice deprecato. Grazie per la risposta, però.
Lucio Mollinedo,

5
@ribsies grazie per la tua nota. Vorrei chiarire qualcosa. Molte altre risposte provano a renderlo semplice . Ma sto cercando di spiegarlo e mostrarlo in uno scenario, chiuso al reale utilizzo . Avremmo bisogno di mettere in cache roba, dovremmo chiamare distruggere alla ri-creazione, ecc. Quindi, mentre la magia della costruzione dinamica è davvero type.builder.tscome hai indicato, vorrei che qualsiasi utente potesse capire come inserire tutto in contesto ... Spero possa essere utile;)
Radim Köhler il

7
@Radim Köhler - Ho provato questo esempio. funziona senza AOT. Ma quando ho provato a farlo eseguire con AOT, viene visualizzato l'errore "Nessun metadata NgModule trovato per RuntimeComponentModule". puoi aiutarmi a risolvere questo errore.
Trusha

4
La risposta in sé è perfetta! Ma per le applicazioni della vita reale non praticabili. Il team angolare dovrebbe fornire una soluzione per questo nel quadro, poiché questo è un requisito comune nelle applicazioni aziendali. In caso contrario, è necessario chiedersi se Angular 2 è la piattaforma giusta per le applicazioni aziendali.
Karl

58

EDIT (26/08/2017) : La soluzione seguente funziona bene con Angular2 e 4. L'ho aggiornata per contenere una variabile modello e gestore di clic e testata con Angular 4.3.
Per Angular4, ngComponentOutlet come descritto nella risposta di Ophir è una soluzione molto migliore. Ma in questo momento non supporta ancora input e output . Se [questo PR] ( https://github.com/angular/angular/pull/15362] è accettato, sarebbe possibile tramite l'istanza del componente restituita dall'evento create.
Ng-dynamic-component potrebbe essere il migliore e il più semplice soluzione del tutto, ma non l'ho ancora testata.

La risposta di @Long Field è perfetta! Ecco un altro esempio (sincrono):

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

In diretta su http://plnkr.co/edit/fdP9Oc .


3
Direi che è un esempio di come scrivere meno codice possibile per fare lo stesso della mia risposta stackoverflow.com/a/38888009/1679310 . Nel caso in cui dovrebbe essere utile (principalmente modello di rigenerazione di RE) quando la condizione cambia ... la semplice ngAfterViewInitchiamata con a const templatenon funzionerà. Ma se il tuo compito era ridurre l'approccio descritto sopra dettagliato (creare un modello, creare un componente, creare un modulo, compilarlo, creare una fabbrica ... creare un'istanza) ... probabilmente l'hai fatto
Radim Köhler,

Grazie per la soluzione: sto riscontrando problemi nel caricamento di templateUrl e degli stili, ricevo il seguente errore: Non è stata fornita alcuna implementazione di ResourceLoader. Non riesci a leggere l'URL localhost: 3000 / app / pages / pages_common.css , hai idea di cosa mi sto perdendo?
Gerardlamo,

È possibile compilare il modello html con dati specifici per la cella in griglia come controllo.? plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview In questo plunker, come posso compilare e mostrare l'immagine nell'ultima colonna.? Qualsiasi aiuto.?
Karthick,

1
@monnef, hai ragione. Non ho controllato il registro della console. Ho modificato il codice per aggiungere il componente in ngOnInit anziché nell'hook ngAfterViewInit, poiché il primo viene attivato prima e il secondo dopo il rilevamento delle modifiche. (Vedi github.com/angular/angular/issues/10131 e discussioni simili.)
Rene Hamburger

1
pulito e semplice. Ha funzionato come previsto durante la pubblicazione sul browser in sviluppo. Ma funziona con AOT? Quando l'app viene eseguita in PROD dopo la compilazione, viene visualizzato il messaggio "Errore: il compilatore di runtime non è caricato" al momento del tentativo di compilazione del componente. (a proposito, sto usando Ionic 3.5)
mymo

52

Devo essere arrivato alla festa tardi, nessuna delle soluzioni qui mi è sembrata utile - troppo disordinata e mi è sembrata una soluzione eccessiva.

Quello che ho finito per fare è utilizzare Angular 4.0.0-beta.6's ngComponentOutlet .

Questo mi ha dato la soluzione più breve e semplice, tutta scritta nel file del componente dinamico.

  • Ecco un semplice esempio che riceve solo testo e lo inserisce in un modello, ma ovviamente puoi cambiare in base alle tue esigenze:
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;
  }
}
  • Breve spiegazione:
    1. my-component - il componente in cui viene eseguito il rendering di un componente dinamico
    2. DynamicComponent - il componente da costruire dinamicamente e viene eseguito il rendering all'interno di my-component

Non dimenticare di aggiornare tutte le librerie angolari a ^ Angular 4.0.0

Spero che questo ti aiuti, buona fortuna!

AGGIORNARE

Funziona anche con angolare 5.


3
Questo ha funzionato benissimo per me con Angular4. L'unica modifica che ho dovuto apportare è stata quella di poter specificare i moduli di importazione per il RuntimeComponentModule creato dinamicamente.
Rahul Patel,

8
Ecco un breve esempio che parte dalla Angular Quickstart: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel

5
Questa soluzione funziona con "ng build --prod"? Sembra che la classe del compilatore e AoT non si adattino insieme atm.
Pierre Chavaroche,

2
@OphirStern Ho anche scoperto che l'approccio funziona bene in Angular 5 ma NON con il flag --prod build.
TaeKwonJoe,

2
L'ho provato con Angular 5 (5.2.8) usando JitCompilerFactory e usando il flag --prod non funziona! Qualcuno ha una soluzione? (BTW JitCompilerFactory senza la bandiera --prod funziona perfettamente)
Frank

20

Risposta di giugno 2019

Grandi notizie! Sembra che il pacchetto @ angular / cdk ora abbia un supporto di prima classe per i portali !

Al momento della stesura di questo documento, non ho trovato particolarmente utili i documenti ufficiali sopra indicati (in particolare per quanto riguarda l'invio e la ricezione di dati dai componenti dinamici). In sintesi, dovrai:

Passaggio 1) Aggiorna il tuo AppModule

Importa PortalModuledal @angular/cdk/portalpacchetto e registra i tuoi componenti dinamici all'internoentryComponents

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

Passaggio 2. Opzione A: se NON è necessario trasferire dati e ricevere eventi dai componenti dinamici :

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

Guardalo in azione

Passaggio 2. Opzione B: se DEVI passare i dati e ricevere eventi dai componenti dinamici :

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

Guardalo in azione


1
Amico, hai appena inchiodato. Questo attirerà l'attenzione. Non riuscivo a credere quanto sia difficile aggiungere un semplice componente dinamico in Angular fino a quando non ne ho bisogno. È come fare il reset e tornare ai tempi pre-JQuery.
Gi1ber7,

2
@ Gi1ber7 lo so giusto? Perché ci sono voluti così tanto?
Stephen Paul,

1
Ottimo approccio, ma sai come passare i parametri a ChildComponent?
Snook,

1
@Snook questo potrebbe rispondere alla tua domanda stackoverflow.com/questions/47469844/…
Stephen Paul

4
@StephenPaul In che cosa Portaldifferisce questo approccio da ngTemplateOutlete ngComponentOutlet? 🤔
Glenn Mohammad,

18

Ho deciso di compattare tutto ciò che ho imparato in un unico file . C'è molto da prendere qui soprattutto rispetto a prima di RC5. Si noti che questo file di origine include AppModule e 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

Ho un semplice esempio per mostrare come eseguire il componente dinamico angolare 2 rc6.

Supponiamo che tu abbia un template html dinamico = template1 e desideri caricare dinamicamente, prima di tutto avvolgere nel componente

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

qui template1 come html, può contenere un componente ng2

Da rc6, devi avere @NgModule a capo questo componente. @NgModule, proprio come il modulo in anglarJS 1, disaccoppia diverse parti dell'applicazione ng2, quindi:

@Component({
  template: template1,

})
class DynamicComponent {

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

(Qui importa RouterModule come nel mio esempio ci sono alcuni componenti di route nel mio html come puoi vedere in seguito)

Ora puoi compilare DynamicModule come: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

E abbiamo bisogno di mettere sopra in app.moudule.ts per caricarlo, si prega di consultare il mio app.moudle.ts. Per maggiori e maggiori dettagli controlla: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts e app.moudle.ts

e vedi la demo: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview


3
Quindi, hai dichiarato module1, module2, module3. E se avessi bisogno di un altro contenuto modello "dinamico", dovresti creare un modulo di definizione (file) moudle4 (module4.ts), giusto? Se sì, ciò non sembra essere dinamico. È statico, no? O mi manca qualcosa?
Radim Köhler,

In sopra "template1" è una stringa di html, puoi inserire qualsiasi cosa al suo interno e chiamiamo questo modello dinamico, come ci si pone questa domanda
Long Field

6

In angolare 7.x ho usato elementi angolari per questo.

  1. Installa @ angular-elements npm i @ angular / elements -s

  2. Creare un servizio accessori.

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

Si noti che il tag dell'elemento personalizzato deve essere diverso con il selettore del componente angolare. in AppUserIconComponent:

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

e in questo caso il nome del tag personalizzato ho usato "user-icon".

  1. Quindi è necessario chiamare il registro in AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. E ora in qualsiasi luogo del tuo codice puoi usarlo in questo modo:
dynamicComponents.create('user-icon', {user:{...}});

o in questo modo:

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

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

(nel modello):

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

Si noti che nel secondo caso è necessario passare oggetti con JSON.stringify e successivamente analizzarlo di nuovo. Non riesco a trovare una soluzione migliore.


Approccio interessante, ma dovrai scegliere come target es2015 (quindi nessun supporto per IE11) nel tuo tsconfig.json, altrimenti fallirà adocument.createElement(tagName);
Snook,

Ciao, come hai menzionato un modo per gestire gli input, quindi anche gli output dei componenti figlio possono essere gestiti in questo modo?
Mustahsan,

5

Risolto questo in Angular 2 versione finale semplicemente usando la direttiva dynamicComponent di ng-dynamic .

Uso:

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

Dove modello è il modello dinamico e il contesto può essere impostato su qualsiasi modello di dati dinamico a cui si desidera associare il modello.


Al momento della stesura di Angular 5 con AOT non è supportato poiché il compilatore JIT non è incluso nel pacchetto. Senza AOT funziona come un fascino :)
Richard Houltz,

questo vale ancora per angolare 7+?
Carlos E,

4

Voglio aggiungere alcuni dettagli in cima a questo eccellente post di Radim.

Ho preso questa soluzione e ci ho lavorato un po 'e ho subito incontrato alcune limitazioni. Tratterò solo quelli e poi darò la soluzione anche a quello.

  • Innanzitutto non sono stato in grado di eseguire il rendering di dettagli dinamici all'interno di un dettaglio dinamico (fondamentalmente nidificare le UI dinamiche l'una all'interno dell'altra).
  • Il problema successivo era che volevo rendere un dettaglio dinamico all'interno di una delle parti rese disponibili nella soluzione. Neanche questo è stato possibile con la soluzione iniziale.
  • Infine, non è stato possibile utilizzare gli URL modello sulle parti dinamiche come l'editor di stringhe.

Ho fatto un'altra domanda basata su questo post, su come raggiungere questi limiti, che può essere trovata qui:

compilazione di modelli dinamici ricorsivi in ​​angular2

Descriverò semplicemente le risposte a queste limitazioni, se dovessi riscontrare lo stesso problema di me, poiché ciò renderebbe la soluzione molto più flessibile. Sarebbe fantastico avere anche il plunker iniziale aggiornato con quello.

Per abilitare l'annidamento dei dettagli dinamici l'uno all'interno dell'altro, è necessario aggiungere DynamicModule.forRoot () nell'istruzione import in type.builder.ts

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

Inoltre, non è stato possibile utilizzare <dynamic-detail>all'interno di una delle parti come editor di stringhe o editor di testo.

Per abilitarlo dovrai cambiare parts.module.tsedynamic.module.ts

All'interno parts.module.tsdovrai aggiungere DynamicDetailil fileDYNAMIC_DIRECTIVES

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

Inoltre, dynamic.module.tsdovresti rimuovere il DynamicDetail in quanto ora fanno parte delle parti

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

Un plunker modificato funzionante può essere trovato qui: http://plnkr.co/edit/UYnQHF?p=preview (Non ho risolto questo problema, sono solo il messaggero :-D)

Infine, non è stato possibile utilizzare templateurls nelle parti create sui componenti dinamici. Una soluzione (o soluzione alternativa. Non sono sicuro che si tratti di un bug angolare o di un uso errato del framework) è stato quello di creare un compilatore nel costruttore invece di iniettarlo.

    private _compiler;

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

Quindi utilizzare la _compilercompilazione, quindi anche i templateUrl sono abilitati.

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

Spero che questo aiuti qualcun altro!

Cordiali saluti Morten


4

In seguito all'eccellente risposta di Radmin, è necessario apportare alcune modifiche a tutti coloro che utilizzano angular-cli versione 1.0.0-beta.22 e successive.

COMPILER_PROVIDERSnon può più essere importato (per i dettagli vedere angular-cli GitHub ).

Quindi la soluzione alternativa è di non usare COMPILER_PROVIDERSe JitCompilernella providerssezione affatto, ma usare JitCompilerFactoryda '@ angular / compilatore' invece come questo all'interno della classe del costruttore di tipi:

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

Come puoi vedere, non è iniettabile e quindi non ha dipendenze con il DI. Questa soluzione dovrebbe funzionare anche per progetti che non usano angular-cli.


1
Grazie per questo suggerimento, tuttavia, mi imbatto in "Nessun metadata NgModule trovato per 'DynamicHtmlModule'". La mia applicazione si basa sul stackoverflow.com/questions/40060498/...
Cybey

2
Qualcuno ha JitCompiletFactory funzionante con campione AOT? Ho lo stesso errore di @Cybey
user2771738


2

Io stesso sto cercando di vedere come posso aggiornare RC4 a RC5 e quindi mi sono imbattuto in questa voce e il nuovo approccio alla creazione di componenti dinamici ha ancora un po 'di mistero per me, quindi non suggerirò nulla sul risolutore di componenti di fabbrica.

Ma ciò che posso suggerire è un approccio un po 'più chiaro alla creazione di componenti in questo scenario: basta usare switch in template che crei l'editor di stringhe o l'editor di testo in base a una condizione, come questa:

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

E a proposito, "[" nell'espressione [prop] ha un significato, questo indica un legame dati a senso unico, quindi puoi e dovresti ometterli nel caso in cui tu sappia che non hai bisogno di associare la proprietà alla variabile.


1
Sarebbe un modo per andare .. se il switch/ casecontiene poche decisioni. Ma immagina che il modello generato potrebbe essere davvero grande ... e differire per ogni entità, differire per sicurezza, differire per stato entità, per ogni tipo di proprietà (numero, data, riferimento ... editor) ... In tal caso, risolverlo con un modello html ngSwitchcreerebbe un file grande, molto molto grande html.
Radim Köhler l'

Oh sono d'accordo con te. Ho questo tipo di scenario proprio qui, in questo momento mentre sto cercando di caricare i principali componenti dell'applicazione senza sapere prima della compilazione particolare classe da visualizzare. Sebbene questo caso particolare non necessiti della creazione di componenti dinamici.
zii,

1

Questo è l'esempio dei controlli dinamici del modulo generati dal server.

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

Questo esempio è dinamico I controlli del modulo sono nel componente aggiuntivo (Qui è possibile ottenere i Formcontrols dal server). Se vedi il metodo addcomponent puoi vedere i controlli dei moduli. In questo esempio non sto usando materiale angolare, ma funziona (sto usando @ lavoro). Questo è indirizzato all'angolo 6, ma funziona in tutte le versioni precedenti.

È necessario aggiungere JITComplierFactory per AngularVersion 5 e successive.

Grazie

Vijay


0

Per questo caso particolare sembra che usare una direttiva per creare dinamicamente il componente sarebbe un'opzione migliore. Esempio:

Nel codice HTML in cui si desidera creare il componente

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

Mi avvicinerei e progetterei la direttiva nel modo seguente.

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

Quindi nei tuoi componenti testo, stringa, data, qualunque cosa - qualunque sia la configurazione che hai passato nell'HTML ng-containernell'elemento sarebbe disponibile.

La configurazione yourConfigpuò essere la stessa e definire i tuoi metadati.

A seconda della configurazione o del tipo di input, la direttiva dovrebbe agire di conseguenza e in base ai tipi supportati, renderebbe il componente appropriato. Altrimenti registrerà un errore.


-1

Sulla base della risposta di Ophir Stern, ecco una variante che funziona con AoT in Angular 4. L'unico problema che ho è che non posso iniettare alcun servizio in DynamicComponent, ma posso conviverci.

nota: non ho ancora testato con 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;

  }

}

Spero che questo ti aiuti.

Saluti!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.