BEARBEITEN - bezogen auf 2.3.0 (07.12.2016)
HINWEIS: Um eine Lösung für die vorherige Version zu erhalten, überprüfen Sie den Verlauf dieses Beitrags
Ein ähnliches Thema wird hier behandelt. Entspricht $ compile in Angular 2 . Wir müssen JitCompiler
und verwenden NgModule
. Lesen NgModule
Sie hier mehr über Angular2:
In einer Nussschale
Es gibt einen funktionierenden Plunker / ein funktionierendes Beispiel (dynamische Vorlage, dynamischer Komponententyp, dynamisches Modul,JitCompiler
, ... in Aktion)
Das Prinzip ist:
1) erstellen Template
2) findet ComponentFactory
im Cache - gehen zu 7)
3) - erstellen Component
4) - erstellen Module
5) - Kompilierung Module
6) - Rückkehr (und Cache für eine spätere Verwendung) ComponentFactory
7) Verwendung Ziel undComponentFactory
eine Instanz zu erstellen von dynamischComponent
Hier ist ein Code-Snippet (mehr davon hier ) - Unser benutzerdefinierter Builder gibt gerade erstellt / zwischengespeichert zurück ComponentFactory
und die Ansicht Zielplatzhalter verbrauchen, um eine Instanz von zu erstellenDynamicComponent
// here we get a TEMPLATE with dynamic content === TODO
var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);
// here we get Factory (just compiled or from cache)
this.typeBuilder
.createComponentFactory(template)
.then((factory: ComponentFactory<IHaveDynamicData>) =>
{
// Target will instantiate and inject component (we'll keep reference to it)
this.componentRef = this
.dynamicComponentTarget
.createComponent(factory);
// let's inject @Inputs to component instance
let component = this.componentRef.instance;
component.entity = this.entity;
//...
});
Das ist es - kurz gesagt. Um mehr Details zu erhalten, lesen Sie unten
.
TL & DR
Beobachten Sie einen Plunker und lesen Sie die Details, falls ein Ausschnitt weitere Erklärungen erfordert
.
Detaillierte Erklärung - Angular2 RC6 ++ & Laufzeitkomponenten
Nachfolgend werden wir dieses Szenario beschreiben
- Erstellen Sie ein Modul
PartsModule:NgModule
(Halter von kleinen Stücken)
- Erstellen Sie ein weiteres Modul
DynamicModule:NgModule
, das unsere dynamische Komponente enthält (und PartsModule
dynamisch referenziert ).
- dynamische Vorlage erstellen (einfacher Ansatz)
- neuen
Component
Typ erstellen (nur wenn sich die Vorlage geändert hat)
- neu erstellen
RuntimeModule:NgModule
. Dieses Modul enthält den zuvor erstellten Component
Typ
- Anruf
JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)
zu bekommenComponentFactory
- Erstellen Sie eine Instanz des
DynamicComponent
Jobs - des Platzhalters "Ziel anzeigen" undComponentFactory
- zuweisen ,
@Inputs
um neue Instanz (Schalter aus INPUT
zu TEXTAREA
bearbeiten) , verbrauchen@Outputs
NgModule
Wir brauchen ein NgModule
s.
Während ich ein sehr einfaches Beispiel zeigen möchte, würde ich in diesem Fall drei Module benötigen (tatsächlich 4 - aber ich zähle das AppModule nicht) . Bitte nehmen Sie dies anstelle eines einfachen Ausschnitts als Grundlage für einen wirklich soliden Generator für dynamische Komponenten.
Es wird ein Modul für alle Kleinteile, zum Beispiel string-editor
, text-editor
( date-editor
, number-editor
...)
@NgModule({
imports: [
CommonModule,
FormsModule
],
declarations: [
DYNAMIC_DIRECTIVES
],
exports: [
DYNAMIC_DIRECTIVES,
CommonModule,
FormsModule
]
})
export class PartsModule { }
Wo DYNAMIC_DIRECTIVES
sind erweiterbar und sollen alle Kleinteile aufnehmen, die für unsere dynamische Komponentenvorlage / unseren dynamischen Komponententyp verwendet werden. Überprüfen Sie app / parts / parts.module.ts
Das zweite wird ein Modul für unser dynamisches Handling sein. Es wird Hosting-Komponenten und einige Anbieter enthalten. Dies werden Singletons sein. Dafür werden wir sie standardmäßig veröffentlichen - mitforRoot()
import { DynamicDetail } from './detail.view';
import { DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';
@NgModule({
imports: [ PartsModule ],
declarations: [ DynamicDetail ],
exports: [ DynamicDetail],
})
export class DynamicModule {
static forRoot()
{
return {
ngModule: DynamicModule,
providers: [ // singletons accross the whole app
DynamicTemplateBuilder,
DynamicTypeBuilder
],
};
}
}
Überprüfen Sie die Verwendung der forRoot()
in derAppModule
Schließlich benötigen wir ein Ad-hoc-Laufzeitmodul. Dieses wird jedoch später als Teil des DynamicTypeBuilder
Jobs erstellt.
Das vierte Modul, das Anwendungsmodul, ist dasjenige, das Compiler-Anbieter deklariert:
...
import { COMPILER_PROVIDERS } from '@angular/compiler';
import { AppComponent } from './app.component';
import { DynamicModule } from './dynamic/dynamic.module';
@NgModule({
imports: [
BrowserModule,
DynamicModule.forRoot() // singletons
],
declarations: [ AppComponent],
providers: [
COMPILER_PROVIDERS // this is an app singleton declaration
],
Lesen Sie viel mehr über NgModule es:
EIN Template Builder
In unserem Beispiel werden wir Details dieser Art von Entität verarbeiten
entity = {
code: "ABC123",
description: "A description of this Entity"
};
Um ein zu erstellen template
, verwenden wir in diesem Plunker diesen einfachen / naiven Builder.
Die eigentliche Lösung, ein echter Vorlagenersteller, ist der Ort, an dem Ihre Anwendung viel leisten kann
// plunker - app/dynamic/template.builder.ts
import {Injectable} from "@angular/core";
@Injectable()
export class DynamicTemplateBuilder {
public prepareTemplate(entity: any, useTextarea: boolean){
let properties = Object.keys(entity);
let template = "<form >";
let editorName = useTextarea
? "text-editor"
: "string-editor";
properties.forEach((propertyName) =>{
template += `
<${editorName}
[propertyName]="'${propertyName}'"
[entity]="entity"
></${editorName}>`;
});
return template + "</form>";
}
}
Ein Trick dabei ist - es erstellt eine Vorlage, die eine Reihe bekannter Eigenschaften verwendet, z entity
. Solche Eigenschaften müssen Teil einer dynamischen Komponente sein, die wir als nächstes erstellen werden.
Um es ein bisschen einfacher zu machen, können wir eine Schnittstelle verwenden, um Eigenschaften zu definieren, die unser Template Builder verwenden kann. Dies wird durch unseren dynamischen Komponententyp implementiert.
export interface IHaveDynamicData {
public entity: any;
...
}
Ein ComponentFactory
Baumeister
Sehr wichtig ist hierbei Folgendes zu beachten:
Unser Komponententyp, der mit unserem erstellt wurde DynamicTypeBuilder
, kann sich unterscheiden - jedoch nur durch die Vorlage (oben erstellt) . Die Eigenschaften der Komponenten (Eingänge, Ausgänge oder einige geschützte) sind immer noch dieselben. Wenn wir unterschiedliche Eigenschaften benötigen, sollten wir eine unterschiedliche Kombination aus Template und Type Builder definieren
Wir berühren also den Kern unserer Lösung. Der Builder wird 1) erstellen ComponentType
2) erstellen NgModule
3) kompilieren ComponentFactory
4) zwischenspeichern für die spätere Wiederverwendung.
Eine Abhängigkeit, die wir erhalten müssen:
// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
@Injectable()
export class DynamicTypeBuilder {
// wee need Dynamic component builder
constructor(
protected compiler: JitCompiler
) {}
Und hier ist ein Ausschnitt, wie man ein ComponentFactory
:
// plunker - app/dynamic/type.builder.ts
// this object is singleton - so we can use this as a cache
private _cacheOfFactories:
{[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};
public createComponentFactory(template: string)
: Promise<ComponentFactory<IHaveDynamicData>> {
let factory = this._cacheOfFactories[template];
if (factory) {
console.log("Module and Type are returned from cache")
return new Promise((resolve) => {
resolve(factory);
});
}
// unknown template ... let's create a Type for it
let type = this.createNewComponent(template);
let module = this.createComponentModule(type);
return new Promise((resolve) => {
this.compiler
.compileModuleAndAllComponentsAsync(module)
.then((moduleWithFactories) =>
{
factory = _.find(moduleWithFactories.componentFactories
, { componentType: type });
this._cacheOfFactories[template] = factory;
resolve(factory);
});
});
}
Oben erstellen und zwischenspeichern wir beide Component
und Module
. Denn wenn die Vorlage (in der Tat der eigentliche dynamische Teil von allem) dieselbe ist, können wir sie wiederverwenden
Und hier sind zwei Methoden, die die wirklich coole Art darstellen, dekorierte Klassen / Typen zur Laufzeit zu erstellen . Nicht nur, @Component
sondern auch die@NgModule
protected createNewComponent (tmpl:string) {
@Component({
selector: 'dynamic-component',
template: tmpl,
})
class CustomDynamicComponent implements IHaveDynamicData {
@Input() public entity: any;
};
// a component for this particular template
return CustomDynamicComponent;
}
protected createComponentModule (componentType: any) {
@NgModule({
imports: [
PartsModule, // there are 'text-editor', 'string-editor'...
],
declarations: [
componentType
],
})
class RuntimeComponentModule
{
}
// a module for just this Type
return RuntimeComponentModule;
}
Wichtig:
Unsere dynamischen Komponententypen unterscheiden sich, jedoch nur nach Vorlage. Wir nutzen diese Tatsache, um sie zwischenzuspeichern. Das ist wirklich sehr wichtig. Angular2 speichert diese auch nach Typ . Und wenn wir für die gleichen Vorlagenzeichenfolgen neue Typen neu erstellen würden ... werden wir anfangen, Speicherlecks zu generieren.
ComponentFactory
wird von der Hosting-Komponente verwendet
Das letzte Stück ist eine Komponente, die das Ziel für unsere dynamische Komponente enthält, z <div #dynamicContentPlaceHolder></div>
. Wir erhalten einen Verweis darauf und ComponentFactory
erstellen damit eine Komponente. Das ist auf den Punkt gebracht, und hier sind alle Teile dieser Komponente (falls erforderlich, öffnen Sie hier den Plunker ).
Fassen wir zunächst die Importanweisungen zusammen:
import {Component, ComponentRef,ViewChild,ViewContainerRef} from '@angular/core';
import {AfterViewInit,OnInit,OnDestroy,OnChanges,SimpleChange} from '@angular/core';
import { IHaveDynamicData, DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';
@Component({
selector: 'dynamic-detail',
template: `
<div>
check/uncheck to use INPUT vs TEXTAREA:
<input type="checkbox" #val (click)="refreshContent(val.checked)" /><hr />
<div #dynamicContentPlaceHolder></div> <hr />
entity: <pre>{{entity | json}}</pre>
</div>
`,
})
export class DynamicDetail implements AfterViewInit, OnChanges, OnDestroy, OnInit
{
// wee need Dynamic component builder
constructor(
protected typeBuilder: DynamicTypeBuilder,
protected templateBuilder: DynamicTemplateBuilder
) {}
...
Wir erhalten nur Builder für Vorlagen und Komponenten. Als nächstes folgen die Eigenschaften, die für unser Beispiel benötigt werden (mehr in den Kommentaren).
// reference for a <div> with #dynamicContentPlaceHolder
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef})
protected dynamicComponentTarget: ViewContainerRef;
// this will be reference to dynamic content - to be able to destroy it
protected componentRef: ComponentRef<IHaveDynamicData>;
// until ngAfterViewInit, we cannot start (firstly) to process dynamic stuff
protected wasViewInitialized = false;
// example entity ... to be recieved from other app parts
// this is kind of candiate for @Input
protected entity = {
code: "ABC123",
description: "A description of this Entity"
};
In diesem einfachen Szenario hat unsere Hosting-Komponente keine @Input
. Es muss also nicht auf Änderungen reagieren. Aber trotz dieser Tatsache (und um auf kommende Änderungen vorbereitet zu sein) - müssen wir ein Flag einführen, wenn die Komponente bereits war (erstens) initiiert wurde. Und nur dann können wir die Magie beginnen.
Schließlich werden wir unseren Komponenten-Builder verwenden, der gerade kompiliert / zwischengespeichert wurde ComponentFacotry
. Unser Zielplatzhalter wird gebeten, dasComponent
mit dieser Fabrik zu instanziieren .
protected refreshContent(useTextarea: boolean = false){
if (this.componentRef) {
this.componentRef.destroy();
}
// here we get a TEMPLATE with dynamic content === TODO
var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);
// here we get Factory (just compiled or from cache)
this.typeBuilder
.createComponentFactory(template)
.then((factory: ComponentFactory<IHaveDynamicData>) =>
{
// Target will instantiate and inject component (we'll keep reference to it)
this.componentRef = this
.dynamicComponentTarget
.createComponent(factory);
// let's inject @Inputs to component instance
let component = this.componentRef.instance;
component.entity = this.entity;
//...
});
}
kleine Erweiterung
Außerdem müssen wir einen Verweis auf die kompilierte Vorlage behalten, um sie ordnungsgemäß zu verwenden destroy()
, wann immer wir sie ändern werden.
// this is the best moment where to start to process dynamic stuff
public ngAfterViewInit(): void
{
this.wasViewInitialized = true;
this.refreshContent();
}
// wasViewInitialized is an IMPORTANT switch
// when this component would have its own changing @Input()
// - then we have to wait till view is intialized - first OnChange is too soon
public ngOnChanges(changes: {[key: string]: SimpleChange}): void
{
if (this.wasViewInitialized) {
return;
}
this.refreshContent();
}
public ngOnDestroy(){
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
getan
Das ist so ziemlich alles. Vergessen Sie nicht, alles zu zerstören , was dynamisch erstellt wurde (ngOnDestroy) . Auch sicher sein , zu Cache dynamisch types
undmodules
wenn der einzige Unterschied ist ihre Vorlage.
Überprüfen Sie sie alle in Aktion hier
Überprüfen Sie den Verlauf , um frühere Versionen (z. B. RC5) dieses Beitrags anzuzeigen