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 JitCompiler
y NgModule
. Lea más sobre NgModule
Angular2 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 ComponentFactory
en 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 ComponentFactory
crear 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é ComponentFactory
y 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
- crear un módulo
PartsModule:NgModule
(titular de piezas pequeñas)
- crear otro módulo
DynamicModule:NgModule
, que contendrá nuestro componente dinámico (y referenciará PartsModule
dinámicamente)
- crear plantilla dinámica (enfoque simple)
- crear un nuevo
Component
tipo (solo si la plantilla ha cambiado)
- crear una nueva
RuntimeModule:NgModule
. Este módulo contendrá el Component
tipo creado previamente
- llamar
JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)
para obtenerComponentFactory
- crear una instancia del
DynamicComponent
trabajo - del marcador de posición Ver destino yComponentFactory
- asignar
@Inputs
a nueva instancia (cambio de INPUT
a TEXTAREA
la edición) , consumen@Outputs
NgModule
Necesitamos un NgModule
s.
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_DIRECTIVES
son 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 DynamicTypeBuilder
trabajo.
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 ComponentFactory
constructor
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á ComponentType
2) creará su NgModule
3) compilará ComponentFactory
4) 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 Component
y 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 @Component
sino 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 ComponentFactory
para 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 types
y modules
si 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