¿Cómo puedo usar / crear plantillas dinámicas para compilar componentes dinámicos con Angular 2.0?

197

Quiero crear dinámicamente una plantilla. Esto debería usarse para construir un ComponentTypetiempo de ejecución y colocarlo (incluso reemplazarlo) en algún lugar dentro del Componente de alojamiento.

Hasta RC4 que estaba usando ComponentResolver, pero con RC5 recibo el siguiente mensaje:

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.

Encontré este documento ( Creación de componentes dinámicos síncronos angulares 2 )

Y entiendo que puedo usar cualquiera

  • Tipo de dinámica ngIfcon ComponentFactoryResolver. Si paso componentes conocidos dentro de @Component({entryComponents: [comp1, comp2], ...})- puedo usar.resolveComponentFactory(componentToRender);
  • Recopilación real en tiempo de ejecución, con Compiler...

Pero la pregunta es cómo usar eso Compiler. La nota anterior dice que debería llamar: Compiler.compileComponentSync/Async¿y cómo?

Por ejemplo. Quiero crear (basado en algunas condiciones de configuración) este tipo de plantilla para un tipo de configuración

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

y en otro caso este ( string-editorse reemplaza por text-editor)

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

Y así sucesivamente (diferente número / fecha / referencia editorspor tipo de propiedad, se omitieron algunas propiedades para algunos usuarios ...) . es decir, este es un ejemplo, la configuración real podría generar plantillas mucho más diferentes y complejas.

La plantilla está cambiando, por lo que no puedo usar ComponentFactoryResolvery pasar las existentes ... Necesito una solución con Compiler.

Radim Köhler
fuente
Dado que la solución que encontré fue tan buena, quiero que todos los que encuentren esta pregunta echen un vistazo a mi respuesta, que está muy por debajo en este momento. :)
Richard Houltz
El artículo Aquí es lo que necesita saber sobre los componentes dinámicos en Angular tiene una gran explicación de los componentes dinámicos.
Max Koretskyi
Aquí está el problema con cada respuesta y qué $compilepodría hacer que estos métodos no puedan: estoy creando una aplicación en la que solo quiero compilar el HTML a medida que entra a través de la página de un tercero y llamadas ajax. No puedo eliminar el HTML de la página y colocarlo en mi propia plantilla. Suspiro
Augie Gardner
@AugieGardner Hay una razón por la cual esto no es posible por diseño. Angular no tiene la culpa de malas decisiones arquitectónicas o sistemas heredados que algunas personas tienen. Si desea analizar el código HTML existente, puede usar otro marco ya que Angular funciona perfectamente bien con WebComponents. Establecer límites claros para guiar a las hordas de programadores inexpertos es más importante que permitir hacks sucios para pocos sistemas heredados.
Phil

Respuestas:

163

EDITAR - relacionado con 2.3.0 (2016-12-07)

NOTA: para obtener una solución para la versión anterior, consulte el historial de esta publicación

Se discute un tema similar aquí Equivalente de $ compilar en Angular 2 . Necesitamos usar JitCompilery NgModule. Lea más sobre NgModuleAngular2 aquí:

En una palabra

Hay un plunker / ejemplo que funciona (plantilla dinámica, tipo de componente dinámico, módulo dinámico JitCompiler, ... en acción)

El principal es:
1) crear plantilla
2) buscar ComponentFactoryen caché - ir a 7)
3) - crear Component
4) - crear Module
5) - compilar Module
6) - devolver (y caché para uso posterior) ComponentFactory
7) usar Target y ComponentFactorycrear una instancia de dinámicaComponent

Aquí hay un fragmento de código (más de esto aquí ) : nuestro generador personalizado regresa recién construido / almacenado en caché ComponentFactoryy la vista Marcador de posición de destino consume para crear una instancia 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;
        //...
    });

Esto es todo, en pocas palabras. Para obtener más detalles ... lea a continuación

.

TL&DR

Observe un saqueador y vuelva a leer los detalles en caso de que algún fragmento requiera más explicación.

.

Explicación detallada - Angular2 RC6 ++ y componentes de tiempo de ejecución

Debajo de la descripción de este escenario , lo haremos

  1. crear un módulo PartsModule:NgModule (titular de piezas pequeñas)
  2. crear otro módulo DynamicModule:NgModule, que contendrá nuestro componente dinámico (y referenciará PartsModuledinámicamente)
  3. crear plantilla dinámica (enfoque simple)
  4. crear un nuevo Componenttipo (solo si la plantilla ha cambiado)
  5. crear una nueva RuntimeModule:NgModule. Este módulo contendrá el Componenttipo creado previamente
  6. llamar JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)para obtenerComponentFactory
  7. crear una instancia del DynamicComponenttrabajo - del marcador de posición Ver destino yComponentFactory
  8. asignar @Inputsa nueva instancia (cambio de INPUTa TEXTAREAla edición) , consumen@Outputs

NgModule

Necesitamos un NgModules.

Si bien me gustaría mostrar un ejemplo muy simple, en este caso, necesitaría tres módulos (de hecho 4, pero no cuento el AppModule) . Por favor, tome esto en lugar de un simple fragmento como base para un generador de componentes dinámicos realmente sólido.

Habrá un módulo para todos los componentes pequeños, por ejemplo string-editor, text-editor ( date-editor, number-editor...)

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

Donde DYNAMIC_DIRECTIVESson extensibles y están destinados a contener todas las piezas pequeñas utilizadas para nuestra plantilla / tipo de Componente dinámico. Verifique app / parts / parts.module.ts

El segundo será el módulo para nuestro manejo dinámico de cosas. Contendrá componentes de hosting y algunos proveedores ... que serán singletons. Por lo tanto, los publicaremos de forma estándar, 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
            ], 
        };
    }
}

Compruebe el uso de forRoot()en elAppModule

Finalmente, necesitaremos un módulo de tiempo de ejecución ad hoc ... pero que se creará más adelante, como parte del DynamicTypeBuildertrabajo.

El cuarto módulo, módulo de aplicación, es el que mantiene declara proveedores de compiladores:

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

Lea (lea) mucho más sobre NgModule allí:

Un generador de plantillas

En nuestro ejemplo procesaremos detalles de este tipo de entidad.

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

Para crear un template, en este plunker usamos este constructor simple / ingenuo.

La solución real, un generador de plantillas real, es el lugar donde su aplicación puede hacer mucho

// 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 truco aquí es: crea una plantilla que utiliza un conjunto de propiedades conocidas, por ejemplo entity. Dichas propiedades deben ser parte del componente dinámico, que crearemos a continuación.

Para hacerlo un poco más fácil, podemos usar una interfaz para definir propiedades, que nuestro generador de plantillas puede usar. Esto será implementado por nuestro tipo de componente dinámico.

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

Un ComponentFactoryconstructor

Lo muy importante aquí es tener en cuenta:

nuestro tipo de componente, compilar con nuestro DynamicTypeBuilder, podría diferir, pero solo por su plantilla (creada anteriormente) . Las propiedades de los componentes (entradas, salidas o algunas protegidas) siguen siendo las mismas. Si necesitamos diferentes propiedades, deberíamos definir diferentes combinaciones de Template y Type Builder

Entonces, estamos tocando el núcleo de nuestra solución. The Builder, 1) creará ComponentType2) creará su NgModule3) compilará ComponentFactory4) la caché para su posterior reutilización.

Una dependencia que necesitamos recibir:

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

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

Y aquí hay un fragmento de cómo obtener 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);
            });
    });
}

Arriba creamos y almacenamos en caché ambos Componenty Module. Porque si la plantilla (de hecho, la parte dinámica real de todo eso) es la misma ... podemos reutilizar

Y aquí hay dos métodos, que representan la forma realmente genial de cómo crear clases / tipos decorados en tiempo de ejecución. No solo @Componentsino también@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:

Nuestros tipos dinámicos de componentes difieren, pero solo por plantilla. Entonces usamos ese hecho para almacenarlos en caché . Esto es realmente muy importante. Angular2 también los almacenará en caché por tipo . Y si recreáramos para la misma plantilla cadenas nuevos tipos ... comenzaremos a generar pérdidas de memoria.

ComponentFactory utilizado por el componente de alojamiento

La pieza final es un componente, que aloja el objetivo de nuestro componente dinámico, por ejemplo <div #dynamicContentPlaceHolder></div>. Obtenemos una referencia y lo usamos ComponentFactorypara crear un componente. En pocas palabras, y aquí están todas las piezas de ese componente (si es necesario, abra el plunker aquí )

Primero resumamos las declaraciones de importación:

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

Acabamos de recibir, constructores de plantillas y componentes. Luego están las propiedades que se necesitan para nuestro ejemplo (más en comentarios)

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

En este escenario simple, nuestro componente de alojamiento no tiene ninguno @Input. Por lo tanto, no tiene que reaccionar a los cambios. Pero a pesar de ese hecho (y para estar listos para los próximos cambios) , necesitamos introducir algún indicador si el componente ya se inició (en primer lugar) . Y solo entonces podemos comenzar la magia.

Finalmente, usaremos nuestro generador de componentes, y simplemente se compilará / almacenará en caché ComponentFacotry . Nuestro marcador de posición de destino se le pedirá crear una instancia de laComponent con la fábrica.

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

pequeña extensión

Además, debemos mantener una referencia a la plantilla compilada ... para poder usarla correctamente destroy(), siempre que la cambiemos.

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

hecho

Eso es básicamente todo. No olvides destruir todo lo que se creó dinámicamente (ngOnDestroy) . Además, asegúrese de almacenar en caché dinámico typesy modulessi la única diferencia es su plantilla.

Compruébalo todo en acción aquí

para ver versiones anteriores (por ejemplo, relacionadas con RC5) de esta publicación, consulte el historial

Radim Köhler
fuente
50
Esto parece una solución tan complicada, la obsoleta era muy simple y clara, ¿hay otra manera de hacer esto?
tibbus
3
Creo que de la misma manera que @tibbus: esto se volvió mucho más complicado de lo que solía ser con el código obsoleto. Gracias por tu respuesta, sin embargo.
Lucio Mollinedo
55
@ribsies gracias por tu nota. Déjame aclarar algo. Muchas otras respuestas intentan hacerlo simple . Pero estoy tratando de explicarlo y mostrarlo en un escenario, cerrado al uso real . Tendríamos que almacenar cosas en caché, tendríamos que llamar a destruir en la recreación, etc. Entonces, mientras que la magia de la construcción dinámica está realmente type.builder.tscomo usted ha señalado, deseo, que cualquier usuario entienda cómo colocar todo eso en contexto ... Espero que pueda ser útil;)
Radim Köhler
77
@Radim Köhler: he probado este ejemplo. Funciona sin AOT. Pero cuando intenté ejecutar esto con AOT, muestra el error "No se encontraron metadatos NgModule para RuntimeComponentModule". ¿puedes ayudarme a resolver este error?
Trusha
44
¡La respuesta en sí es perfecta! Pero para aplicaciones de la vida real no es factible. El equipo angular debe proporcionar una solución para esto en el marco, ya que este es un requisito común en las aplicaciones empresariales. De lo contrario, debe preguntarse si Angular 2 es la plataforma adecuada para aplicaciones comerciales.
Karl
58

EDITAR (26/08/2017) : La solución a continuación funciona bien con Angular2 y 4. Lo actualicé para contener una variable de plantilla y haga clic en el controlador y lo probé con Angular 4.3.
Para Angular4, ngComponentOutlet como se describe en la respuesta de Ophir es una solución mucho mejor. Pero en este momento todavía no admite entradas y salidas . Si se acepta [este PR] ( https://github.com/angular/angular/pull/15362] , sería posible a través de la instancia del componente devuelta por el evento create.
Ng-dynamic-component puede ser el mejor y más simple solución por completo, pero aún no lo he probado.

¡La respuesta de @Long Field es acertada! Aquí hay otro ejemplo (sincrónico):

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

En vivo en http://plnkr.co/edit/fdP9Oc .

Rene Hamburger
fuente
3
Diría que es un ejemplo de cómo escribir la menor cantidad de código posible para hacer lo mismo que en mi respuesta stackoverflow.com/a/38888009/1679310 . En caso de que sea útil (en su mayoría plantilla RE-Generadora) cuando la condición cambia ... la simple ngAfterViewInitllamada con a const templateno funcionará. Pero si su tarea era reducir el enfoque descrito anteriormente detallado (crear plantilla, crear componente, crear módulo, compilarlo, crear fábrica ... crear instancia) ... probablemente lo hizo
Radim Köhler
Gracias por la solución: tengo problemas para cargar templateUrl y estilos, sin embargo, aparece el siguiente error: no se ha proporcionado ninguna implementación de ResourceLoader. No puedo leer la url localhost: 3000 / app / pages / pages_common.css , ¿alguna idea de lo que me falta?
Gerardlamo
¿Podría ser posible compilar la plantilla html con datos específicos para la celda en la cuadrícula como control? plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview En este plunker, ¿cómo puedo compilar y mostrar la imagen en la última columna? Alguna ayuda.?
Karthick
1
@monnef, tienes razón. No revisé el registro de la consola. Ajusté el código para agregar el componente en el ngOnInit en lugar del ngAfterViewInit, ya que el primero se activa antes y el segundo después de la detección del cambio. (Ver github.com/angular/angular/issues/10131 e hilos similares.)
Rene Hamburger
1
ordenado y simple. Funcionó como se esperaba al servir sobre el navegador en dev. ¿Pero esto funciona con AOT? Cuando la aplicación se ejecuta en PROD después de la compilación, aparece el mensaje "Error: el compilador de tiempo de ejecución no está cargado" en el momento en que se intenta la compilación del componente. (por cierto, estoy usando Ionic 3.5)
mymo
52

Debo haber llegado tarde a la fiesta, ninguna de las soluciones aquí me pareció útil, demasiado desordenada y me pareció una solución alternativa.

Lo que terminé haciendo es utilizar Angular 4.0.0-beta.6's ngComponentOutlet .

Esto me dio la solución más corta y simple escrita en el archivo del componente dinámico.

  • Aquí hay un ejemplo simple que solo recibe texto y lo coloca en una plantilla, pero obviamente puede cambiar según su necesidad:
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 explicación:
    1. my-component - el componente en el que se representa un componente dinámico
    2. DynamicComponent - el componente que se construirá dinámicamente y se renderiza dentro de my-component

No olvide actualizar todas las bibliotecas angulares a ^ Angular 4.0.0

Espero que esto ayude, buena suerte!

ACTUALIZAR

También funciona para angular 5.

Ophir Stern
fuente
3
Esto funcionó muy bien para mí con Angular4. El único ajuste que tuve que hacer fue poder especificar módulos de importación para el RuntimeComponentModule creado dinámicamente.
Rahul Patel
8
Aquí hay un ejemplo rápido a partir del inicio rápido angular: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel
55
¿Esta solución funciona con "ng build --prod"? Parece que la clase del compilador y AoT no encajan en el cajero automático.
Pierre Chavaroche
2
@OphirStern También descubrí que este enfoque funciona bien en Angular 5 pero NO con el indicador de compilación --prod.
TaeKwonJoe
2
¡Lo probé con angular 5 (5.2.8) usando JitCompilerFactory y usando la bandera --prod no funciona! ¿Alguien tiene una solución? (Por cierto, JitCompilerFactory sin la bandera --prod funciona perfectamente)
Frank
20

Respuesta de junio de 2019

¡Una gran noticia! ¡Parece que el paquete @ angular / cdk ahora tiene soporte de primera clase para portales !

Al momento de escribir esto, no encontré los documentos oficiales anteriores particularmente útiles (particularmente con respecto al envío de datos y la recepción de eventos desde los componentes dinámicos). En resumen, deberá:

Paso 1) Actualiza tu AppModule

Importa PortalModuledesde el @angular/cdk/portalpaquete y registra tus componentes dinámicos dentroentryComponents

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

Paso 2. Opción A: si NO necesita pasar datos y recibir eventos de sus componentes dinámicos :

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

Véalo en acción

Paso 2. Opción B: si necesita pasar datos y recibir eventos de sus componentes dinámicos :

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

Véalo en acción

Stephen Paul
fuente
1
Amigo, acabas de clavar. Este llamará la atención. No podía creer lo difícil que es agregar un componente dinámico simple en Angular hasta que tuve que hacer uno. Es como reiniciar y volver a los tiempos anteriores a JQuery.
Gi1ber7
2
@ Gi1ber7 Lo sé ¿verdad? ¿Por qué les tomó tanto tiempo?
Stephen Paul
1
Buen enfoque, pero ¿sabes cómo pasar parámetros a ChildComponent?
Snook
1
@Busque esto puede responder a su pregunta stackoverflow.com/questions/47469844/…
Stephen Paul
44
@StephenPaul ¿Cómo Portaldifiere este enfoque de ngTemplateOutlety ngComponentOutlet? 🤔
Glenn Mohammad
18

Decidí compactar todo lo que aprendí en un solo archivo . Hay mucho que ver aquí, especialmente en comparación con antes de RC5. Tenga en cuenta que este archivo fuente incluye AppModule y 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 {}`
Stephen Paul
fuente
10

Tengo un ejemplo simple para mostrar cómo hacer un componente dinámico angular 2 rc6.

Supongamos que tiene una plantilla html dinámica = plantilla1 y desea cargar dinámicamente, primero envolver en el componente

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

aquí template1 como html, puede contener un componente ng2

Desde rc6, debe tener @NgModule para envolver este componente. @NgModule, al igual que el módulo en anglarJS 1, desacopla diferentes partes de la aplicación ng2, por lo que:

@Component({
  template: template1,

})
class DynamicComponent {

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

(Aquí importa RouterModule ya que en mi ejemplo hay algunos componentes de ruta en mi html como puedes ver más adelante)

Ahora puede compilar DynamicModule como: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

Y necesitamos ponerlo arriba en app.moudule.ts para cargarlo, vea mi app.moudle.ts. Para obtener más detalles, consulte: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts y app.moudle.ts

y vea la demostración: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

Campo largo
fuente
3
Entonces, has declarado module1, module2, module3. Y si necesitara otro contenido de plantilla "dinámico", necesitaría crear un formulario de definición (archivo) moudle4 (module4.ts), ¿verdad? Si es así, eso no parece ser dinámico. Es estático, ¿no es así? ¿O extraño algo?
Radim Köhler
En "template1" anterior hay una cadena de html, puede poner cualquier cosa y llamamos a esta plantilla dinámica, ya que esta pregunta es
Long Field,
6

En angular 7.x usé elementos angulares para esto.

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

  2. Crear servicio de accesorios.

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

Tenga en cuenta que su etiqueta de elemento personalizado debe ser diferente con el selector de componente angular. en AppUserIconComponent:

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

y en este caso, el nombre de la etiqueta personalizada usé "icono de usuario".

  1. Luego debe llamar a registrarse en AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. Y ahora, en cualquier lugar de su código, puede usarlo así:
dynamicComponents.create('user-icon', {user:{...}});

o así:

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

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

(en plantilla):

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

Tenga en cuenta que en el segundo caso debe pasar objetos con JSON.stringify y luego analizarlo nuevamente. No puedo encontrar una mejor solución.

Oleg Pnk
fuente
Enfoque interesante, pero tendrá que apuntar a es2015 (por lo que no es compatible con IE11) en su tsconfig.json, de lo contrario, fallará endocument.createElement(tagName);
Snook
Hola, como mencionó una forma de manejar las entradas, ¿pueden las salidas de los componentes secundarios también manejarse de esta manera?
Mustahsan el
5

Resolvió esto en la versión Angular 2 Final simplemente usando la directiva dynamicComponent de ng-dynamic .

Uso:

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

Donde template es su plantilla dinámica y el contexto se puede establecer en cualquier modelo de datos dinámico al que desee que se una su plantilla.

Richard Houltz
fuente
En el momento de escribir, Angular 5 con AOT no lo admite, ya que el compilador JIT no está incluido en el paquete. Sin AOT funciona como un encanto :)
Richard Houltz
¿esto todavía se aplica a angular 7+?
Carlos E
4

Quiero agregar algunos detalles sobre esta excelente publicación de Radim.

Tomé esta solución y trabajé un poco en ella y rápidamente encontré algunas limitaciones. Solo describiré eso y luego daré la solución a eso también.

  • En primer lugar, no pude representar el detalle dinámico dentro de un detalle dinámico (básicamente anidar interfaces de usuario dinámicas entre sí).
  • El siguiente problema fue que quería representar un detalle dinámico dentro de una de las partes que estaba disponible en la solución. Eso tampoco fue posible con la solución inicial.
  • Por último, no fue posible utilizar URL de plantilla en las partes dinámicas como el editor de cadenas.

Hice otra pregunta basada en esta publicación, sobre cómo lograr estas limitaciones, que se pueden encontrar aquí:

compilación dinámica recursiva de plantillas en angular2

Solo describiré las respuestas a estas limitaciones, si se encuentra con el mismo problema que yo, ya que eso hace que la solución sea mucho más flexible. Sería increíble tener el plunker inicial actualizado con eso también.

Para habilitar el anidamiento dinámico de detalles entre sí, deberá agregar DynamicModule.forRoot () en la declaración de importación en 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;
}

Además de eso, no fue posible usar <dynamic-detail>dentro de una de las partes que es editor de cadenas o editor de texto.

Para habilitarlo, deberá cambiar parts.module.tsydynamic.module.ts

En el interior parts.module.tsdeberás agregar DynamicDetailelDYNAMIC_DIRECTIVES

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

También en el dynamic.module.tstendría que eliminar el DynamicDetail ya que ahora son parte de las partes

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

Puede encontrar un plunker modificado aquí: http://plnkr.co/edit/UYnQHF?p=preview (No resolví este problema, solo soy el mensajero :-D)

Finalmente, no fue posible utilizar templateurls en las partes creadas en los componentes dinámicos. Una solución (o solución alternativa. No estoy seguro de si es un error angular o un uso incorrecto del marco) fue crear un compilador en el constructor en lugar de inyectarlo.

    private _compiler;

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

Luego use _compilerpara compilar, luego templateUrls también están habilitados.

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

¡Espero que esto ayude a alguien más!

Saludos cordiales Morten

Morten Skjoldager
fuente
4

Siguiendo la excelente respuesta de Radmin, se necesita un pequeño ajuste para todos los que usan angular-cli versión 1.0.0-beta.22 y superior.

COMPILER_PROVIDERSya no se puede importar (para más detalles, consulte angular-cli GitHub ).

Entonces, la solución alternativa es no usar COMPILER_PROVIDERSy JitCompileren la providerssección, sino usar JitCompilerFactorydesde '@ angular / compilador' en su lugar dentro de la clase de generador de tipos:

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

Como puede ver, no es inyectable y, por lo tanto, no tiene dependencias con el DI. Esta solución también debería funcionar para proyectos que no usan angular-cli.

Sebastian
fuente
1
Gracias por esta sugerencia, sin embargo, me encuentro con "No se encontraron metadatos NgModule para 'DynamicHtmlModule'". Mi implementación se basa en stackoverflow.com/questions/40060498/…
Cybey
2
¿Alguien ha trabajado JitCompiletFactory con muestra AOT? Tengo el mismo error que @Cybey
usuario2771738
2

Yo mismo estoy tratando de ver cómo puedo actualizar RC4 a RC5 y, por lo tanto, me topé con esta entrada y el nuevo enfoque para la creación de componentes dinámicos todavía tiene un poco de misterio para mí, por lo que no sugeriré nada sobre la resolución de fábrica de componentes.

Pero, lo que puedo sugerir es un enfoque un poco más claro para la creación de componentes en este escenario: solo use el interruptor en la plantilla que crearía un editor de cadenas o un editor de texto de acuerdo con alguna condición, como esta:

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

Y, por cierto, "[" en la expresión [prop] tiene un significado, esto indica un enlace de datos unidireccional, por lo tanto, puede e incluso debe omitirlos en caso de que sepa que no necesita vincular la propiedad a la variable.

zii
fuente
1
Eso sería un camino a seguir ... si el switch/case contiene pocas decisiones. Pero imagine que la plantilla generada podría ser realmente grande ... y diferir para cada entidad, diferir por seguridad, diferir por estado de entidad, por cada tipo de propiedad (número, fecha, referencia ... editores) ... En tal caso, resolver esto en una plantilla html ngSwitchcrearía un htmlarchivo grande, muy muy grande .
Radim Köhler
Oh estoy de acuerdo contigo Tengo este tipo de escenario aquí, ahora mismo, ya que estoy tratando de cargar los principales componentes de la aplicación sin saber antes de la compilación de la clase particular que se mostrará. Aunque este caso particular no necesita la creación dinámica de componentes.
zii
1

Este es el ejemplo de controles dinámicos de formularios generados desde el servidor.

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

Este ejemplo es dinámico. Los controles de formulario están en el componente de agregar (aquí es donde puede obtener los controles de formulario del servidor). Si ve el método addcomponent, puede ver los controles de formularios. En este ejemplo, no estoy usando material angular, pero funciona (estoy usando @ work). Este es el objetivo de angular 6, pero funciona en todas las versiones anteriores.

Necesita agregar JITComplierFactory para AngularVersion 5 y superior.

Gracias

Vijay

Vijay Anand Kannan
fuente
0

Para este caso particular, parece que usar una directiva para crear dinámicamente el componente sería una mejor opción. Ejemplo:

En el HTML donde desea crear el componente

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

Me acercaría y diseñaría la directiva de la siguiente manera.

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

Entonces, en sus componentes, texto, cadena, fecha, lo que sea, sea cual sea la configuración que haya pasado en el HTML en el ng-container elemento estaría disponible.

La configuración, yourConfig puede ser la misma y definir sus metadatos.

Dependiendo de su configuración o tipo de entrada, la directiva debería actuar en consecuencia y, de los tipos admitidos, representaría el componente apropiado. Si no, registrará un error.

dijoutt
fuente
-1

Sobre la base de la respuesta de Ophir Stern, aquí hay una variante que funciona con AoT en Angular 4. El único problema que tengo es que no puedo inyectar ningún servicio en DynamicComponent, pero puedo vivir con eso.

nota: no he probado 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;

  }

}

Espero que esto ayude.

¡Salud!

Volcán
fuente