EDIT - lié à 2.3.0 (2016-12-07)
REMARQUE: pour obtenir une solution pour la version précédente, consultez l'historique de ce message
Un sujet similaire est discuté ici Equivalent de $ compile dans Angular 2 . Nous devons utiliser JitCompiler
et NgModule
. En savoir plus sur NgModule
Angular2 ici:
En un mot
Il existe un plunker / exemple fonctionnel (modèle dynamique, type de composant dynamique, module dynamique ,, ... en action)JitCompiler
Le principe est:
1) créer un modèle
2) trouver ComponentFactory
dans le cache - aller à 7)
3) - créer Component
4) - créer Module
5) - compiler Module
6) - retourner (et mettre en cache pour une utilisation ultérieure) ComponentFactory
7) utiliser Target et ComponentFactory
créer une instance de dynamiqueComponent
Voici un extrait de code (plus ici ) - Notre générateur personnalisé retourne juste construit / mis en cache ComponentFactory
et l'espace réservé de vue cible consomme pour créer une instance de laDynamicComponent
// 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;
//...
});
Ça y est - en bref. Pour obtenir plus de détails .. lire ci-dessous
.
TL&DR
Observez un plongeur et revenez pour lire les détails au cas où un extrait nécessiterait plus d'explications
.
Explication détaillée - Angular2 RC6 ++ et composants d'exécution
Ci-dessous la description de ce scénario , nous allons
- créer un module
PartsModule:NgModule
(support de petites pièces)
- créer un autre module
DynamicModule:NgModule
, qui contiendra notre composant dynamique (et référencera PartsModule
dynamiquement)
- créer un modèle dynamique (approche simple)
- créer un nouveau
Component
type (uniquement si le modèle a changé)
- créer de nouveaux
RuntimeModule:NgModule
. Ce module contiendra le Component
type précédemment créé
- appeler
JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)
pour obtenirComponentFactory
- créer une instance de la
DynamicComponent
tâche - de l'espace réservé View Target etComponentFactory
- attribuer
@Inputs
à nouvelle instance (commutateur de INPUT
de TEXTAREA
montage) , à consommer@Outputs
NgModule
Nous avons besoin d'un NgModule
s.
Alors que je voudrais montrer un exemple très simple, dans ce cas, j'aurais besoin de trois modules (en fait 4 - mais je ne compte pas l'AppModule) . Veuillez prendre ceci plutôt qu'un simple extrait de code comme base pour un générateur de composants dynamiques vraiment solide.
Il y aura un module pour tous les petits composants, par exemple string-editor
, text-editor
( date-editor
, number-editor
...)
@NgModule({
imports: [
CommonModule,
FormsModule
],
declarations: [
DYNAMIC_DIRECTIVES
],
exports: [
DYNAMIC_DIRECTIVES,
CommonModule,
FormsModule
]
})
export class PartsModule { }
Où DYNAMIC_DIRECTIVES
sont extensibles et sont destinés à contenir toutes les petites pièces utilisées pour notre modèle / type de composant dynamique. Vérifiez app / parts / parts.module.ts
Le second sera un module pour notre gestion dynamique des choses. Il contiendra des composants d'hébergement et certains fournisseurs .. qui seront des singletons. Nous les publierons donc de manière standard - avecforRoot()
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
],
};
}
}
Vérifiez l'utilisation du forRoot()
auAppModule
Enfin, nous aurons besoin d'un module d'exécution adhoc .. mais qui sera créé plus tard, dans le cadre du DynamicTypeBuilder
travail.
Le quatrième module, module d'application, est celui qui continue de déclarer les fournisseurs de compilateur:
...
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
],
Lisez (lisez) beaucoup plus sur NgModule ici:
Un constructeur de modèles
Dans notre exemple, nous traiterons les détails de ce type d' entité
entity = {
code: "ABC123",
description: "A description of this Entity"
};
Pour créer un template
, dans ce plunker, nous utilisons ce constructeur simple / naïf.
La vraie solution, un vrai constructeur de modèles, est l'endroit où votre application peut faire beaucoup
// 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>";
}
}
Une astuce ici est - il construit un modèle qui utilise un ensemble de propriétés connues, par exemple entity
. Ces propriétés doivent faire partie d'un composant dynamique, que nous créerons ensuite.
Pour le rendre un peu plus facile, nous pouvons utiliser une interface pour définir les propriétés, que notre constructeur de modèles peut utiliser. Ceci sera implémenté par notre type de composant dynamique.
export interface IHaveDynamicData {
public entity: any;
...
}
Un ComponentFactory
constructeur
La chose très importante ici est de garder à l'esprit:
notre type de composant, construit avec notre DynamicTypeBuilder
, pourrait différer - mais uniquement par son modèle (créé ci-dessus) . Les propriétés des composants (entrées, sorties ou certaines protégées) sont toujours les mêmes. Si nous avons besoin de propriétés différentes, nous devons définir une combinaison différente de Template et Type Builder
Nous touchons donc au cœur de notre solution. Le Builder, 1) créera ComponentType
2) créera sa NgModule
3) compilera ComponentFactory
4) le mettra en cache pour une réutilisation ultérieure.
Une dépendance que nous devons recevoir:
// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
@Injectable()
export class DynamicTypeBuilder {
// wee need Dynamic component builder
constructor(
protected compiler: JitCompiler
) {}
Et voici un extrait comment obtenir 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);
});
});
}
Ci-dessus, nous créons et mettons en cache à la fois Component
et Module
. Parce que si le modèle (en fait la vraie partie dynamique de tout cela) est le même .. nous pouvons réutiliser
Et voici deux méthodes, qui représentent la manière vraiment cool de créer des classes / types décorés en runtime. Non seulement @Component
mais aussi@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;
}
Important:
nos types de composants dynamiques diffèrent, mais uniquement par modèle. Nous utilisons donc ce fait pour les mettre en cache . C'est vraiment très important. Angular2 mettra également ces derniers en cache par type . Et si nous recréions pour les mêmes chaînes de modèles de nouveaux types ... nous commencerons à générer des fuites de mémoire.
ComponentFactory
utilisé par le composant d'hébergement
La pièce finale est un composant, qui héberge la cible de notre composant dynamique, par exemple <div #dynamicContentPlaceHolder></div>
. Nous obtenons une référence et utilisons ComponentFactory
pour créer un composant. C'est en un mot, et voici toutes les pièces de ce composant (si nécessaire, ouvrez le plongeur ici )
Résumons d'abord les instructions d'importation:
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
) {}
...
Nous venons de recevoir, les créateurs de modèles et de composants. Viennent ensuite les propriétés qui sont nécessaires pour notre exemple (plus dans les commentaires)
// 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"
};
Dans ce scénario simple, notre composant d'hébergement n'en a pas @Input
. Il n'a donc pas à réagir aux changements. Mais malgré cela (et pour être prêt pour les changements à venir) - nous devons introduire un indicateur si le composant a déjà (tout d'abord) été lancé. Et alors seulement, nous pouvons commencer la magie.
Enfin, nous utiliserons notre constructeur de composants, et il vient d'être compilé / mis en cache ComponentFacotry
. Notre espace réservé cible sera invité à instancier leComponent
avec cette usine.
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;
//...
});
}
petite extension
De plus, nous devons conserver une référence au modèle compilé .. pour pouvoir le correctement destroy()
, chaque fois que nous le changerons.
// 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;
}
}
terminé
C'est à peu près tout. N'oubliez pas de détruire tout ce qui a été construit dynamiquement (ngOnDestroy) . Assurez-vous également de mettre en cache dynamique types
et modules
si la seule différence est leur modèle.
Vérifiez tout en action ici
pour voir les versions précédentes (par exemple liées à RC5) de ce post, consultez l' historique