javascript - Angular 2.1.0 crea componentes secundarios sobre la marcha, dinĂ¡micamente
(1)
Puede usar la siguiente directiva
HtmlOutlet
:
import {
Component,
Directive,
NgModule,
Input,
ViewContainerRef,
Compiler,
ComponentFactory,
ModuleWithComponentFactories,
ComponentRef,
ReflectiveInjector
} from ''@angular/core'';
import { RouterModule } from ''@angular/router'';
import { CommonModule } from ''@angular/common'';
export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> {
const cmpClass = class DynamicComponent {};
const decoratedCmp = Component(metadata)(cmpClass);
@NgModule({ imports: [CommonModule, RouterModule], declarations: [decoratedCmp] })
class DynamicHtmlModule { }
return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
.then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => {
return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
});
}
@Directive({ selector: ''html-outlet'' })
export class HtmlOutlet {
@Input() html: string;
cmpRef: ComponentRef<any>;
constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }
ngOnChanges() {
const html = this.html;
if (!html) return;
if(this.cmpRef) {
this.cmpRef.destroy();
}
const compMetadata = new Component({
selector: ''dynamic-html'',
template: this.html,
});
createComponentFactory(this.compiler, compMetadata)
.then(factory => {
const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
});
}
ngOnDestroy() {
if(this.cmpRef) {
this.cmpRef.destroy();
}
}
}
Ver también Ejemplo de Plunker
Ejemplo con componente personalizado
Para la compilación AOT ver estos hilos
- https://github.com/angular/angular/issues/15510
- http://blog.assaf.co/angular-2-harmony-aot-compilation-with-lazy-jit-2/
Consulte también el ejemplo de AOT de github Webpack https://github.com/alexzuza/angular2-build-examples/tree/master/ngc-webpack
Lo que estoy tratando de hacer en
angular 2.1.0
es crear componentes secundarios sobre la marcha que deberían inyectarse en el componente principal.
Por ejemplo, el componente principal es
lessonDetails
que contiene cosas compartidas para todas las lecciones, como botones como
Go to previous lesson
,
Go to next lesson
y otras cosas.
Según los parámetros de ruta, el contenido de la lección, que debe ser
un componente secundario,
debe inyectarse dinámicamente en el
componente principal
.
HTML para componentes secundarios (contenido de la lección) se define como una cadena simple en algún lugar externo, puede ser un objeto como:
export const LESSONS = {
"lesson-1": `<p> lesson 1 </p>`,
"lesson-2": `<p> lesson 2 </p>`
}
El problema se puede resolver fácilmente a través de
innerHtml
tiene algo como seguir en
la
plantilla del
componente principal
.
<div [innerHTML]="lessonContent"></div>
Donde en cada cambio de parámetros de ruta, la
lessonContent
de propiedad El
lessonContent
del componente principal cambiaría (el contenido (nueva plantilla) se tomaría del objeto
LESSON
) haciendo que la plantilla del componente principal se actualice.
Esto funciona, pero angular no procesará el contenido inyectado a través de
innerHtml
por lo que es imposible usar
routerLink
y otras cosas.
Antes de la nueva versión angular resolví este problema usando la solución de
http://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/
, donde he estado usando
ComponentMetadata
junto con
ComponentResolver
para crear componentes secundarios en la mosca, como:
const metadata = new ComponentMetadata({
template: this.templateString,
});
Donde
templateString
se pasó al componente secundario como propiedad de
Input
al componente secundario.
Tanto
MetaData
como
ComponentResolver
están en desuso / eliminados en
angular 2.1.0
.
Entonces, el problema no se trata solo de la creación dinámica de componentes, como se describe en algunas preguntas SO relacionadas, el problema sería más fácil de resolver si hubiera definido un componente para cada contenido de la lección. Esto significaría que necesito predeclarar 100 componentes diferentes para 100 lecciones diferentes. Los metadatos en desuso proporcionaron un comportamiento que era como actualizar la plantilla en tiempo de ejecución de un solo componente (crear y destruir un solo componente en el cambio de parámetros de ruta).
Actualización 1:
Como parece en la versión angular reciente, todos los componentes que deben crearse / inyectarse dinámicamente deben predefinirse en
entryComponents
dentro de
@NgModule
.
Entonces, como me parece, en relación con la pregunta anterior, si necesito tener 100 lecciones (componentes que deben crearse dinámicamente sobre la marcha), eso significa que necesito predefinir 100 componentes
Actualización 2:
según la Actualización 1, se puede hacer a través de
ViewContainerRef.createComponent()
de la siguiente manera:
// lessons.ts
@Component({ template: html string loaded from somewhere })
class LESSON_1 {}
@Component({ template: html string loaded from somewhere })
class LESSON_2 {}
// exported value to be used in entryComponents in @NgModule
export const LESSON_CONTENT_COMPONENTS = [ LESSON_1, LESSON_2 ]
Ahora en el componente principal en el cambio de parámetros de ruta
const key = // determine lesson name from route params
/**
* class is just buzzword for function
* find Component by name (LESSON_1 for example)
* here name is property of function (class)
*/
const dynamicComponent = _.find(LESSON_CONTENT_COMPONENTS, { name: key });
const lessonContentFactory = this.resolver.resolveComponentFactory(dynamicComponent);
this.componentRef = this.lessonContent.createComponent(lessonContentFactory);
La plantilla principal se ve así:
<div *ngIf="something" #lessonContentContainer></div>
Donde
lessonContentContainer
está decorada
@ViewChildren
propiedad
@ViewChildren
y
lessonContent
está decorada como
@ViewChild
y se inicializa en
ngAfterViewInit ()
como:
ngAfterViewInit () {
this.lessonContentContainer.changes.subscribe((items) => {
this.lessonContent = items.first;
this.subscription = this.activatedRoute.params.subscribe((params) => {
// logic that needs to show lessons
})
})
}
La solución tiene un inconveniente
y es que todos los componentes (LESSON_CONTENT_COMPONENTS) deben estar predefinidos.
¿Hay alguna manera de usar un solo componente y cambiar la plantilla de ese componente en tiempo de ejecución (en el cambio de parámetros de ruta)?