EDIT - 2.3.0相关(2016-12-07)
注意:要获得以前版本的解决方案,请检查这篇文章的历史
这里讨论了类似的主题,相当于Angular 2中的$compile。我们需要使用JitCompiler和NgModule。在这里阅读更多关于Angular2中的NgModule:
Angular 2 RC5 - NgModules、惰性加载和AoT编译
简而言之
有一个工作的活塞/例子(动态模板,动态组件类型,动态模块,JitCompiler,…在行动)
其原理是:
1)创建模板
2)在缓存中找到ComponentFactory -转到7)
3) -创建组件
4) -创建模块
5) -编译模块
6) -返回(和缓存供以后使用)ComponentFactory
7)使用Target和ComponentFactory创建一个动态组件的实例
下面是一个代码片段(更多的代码在这里)-我们的自定义生成器返回刚刚构建/缓存的ComponentFactory和视图目标占位符消费,以创建一个DynamicComponent的实例
// 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;
//...
});
简单地说,就是这样。欲了解更多详情..阅读下面的
.
TL&DR
观察一个活塞,然后回来阅读细节,以防一些片段需要更多的解释
.
详细说明- Angular2 rc6++ &运行时组件
下面对这个场景进行描述
create a module PartsModule:NgModule (holder of small pieces)
create another module DynamicModule:NgModule, which will contain our dynamic component (and reference PartsModule dynamically)
create dynamic Template (simple approach)
create new Component type (only if template has changed)
create new RuntimeModule:NgModule. This module will contain the previously created Component type
call JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) to get ComponentFactory
create an Instance of the DynamicComponent - job of the View Target placeholder and ComponentFactory
assign @Inputs to new instance (switch from INPUT to TEXTAREA editing), consume @Outputs
NgModule
我们需要一个ngmodule。
虽然我想展示一个非常简单的例子,但在这种情况下,我需要三个模块(实际上是4个——但我没有计算AppModule)。请将本文而不是简单的代码片段作为真正可靠的动态组件生成器的基础。
所有小组件都有一个模块,例如字符串编辑器、文本编辑器(日期编辑器、数字编辑器……)
@NgModule({
imports: [
CommonModule,
FormsModule
],
declarations: [
DYNAMIC_DIRECTIVES
],
exports: [
DYNAMIC_DIRECTIVES,
CommonModule,
FormsModule
]
})
export class PartsModule { }
其中DYNAMIC_DIRECTIVES是可扩展的,用于保存动态组件模板/类型所使用的所有小部件。检查应用程序/部分/ parts.module.ts
第二个将是模块为我们的动态东西处理。它将包含托管组件和一些提供商..这将是单身。因此,我们将以标准方式发布它们——使用forRoot()
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
],
};
}
}
检查AppModule中forRoot()的使用情况
最后,我们将需要一个adhoc,运行时模块。但它将在后面创建,作为DynamicTypeBuilder作业的一部分。
第四个模块,应用程序模块,是一个声明编译器提供程序的模块:
...
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
],
请阅读(务必阅读)更多关于NgModule的内容:
Angular 2 RC5 - NgModules、惰性加载和AoT编译
Angular模块文档
模板构建器
在我们的示例中,我们将处理这种实体的详细信息
entity = {
code: "ABC123",
description: "A description of this Entity"
};
为了创建一个模板,在这个活塞中,我们使用这个简单/幼稚的构建器。
真正的解决方案,真正的模板构建器,是应用程序可以做很多事情的地方
// 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>";
}
}
这里的一个技巧是——它构建了一个使用一些已知属性集的模板,例如实体。这样的属性(-ies)必须是我们接下来创建的动态组件的一部分。
为了更简单一点,我们可以使用一个接口来定义属性,我们的模板构建器可以使用这些属性。这将由我们的动态Component类型实现。
export interface IHaveDynamicData {
public entity: any;
...
}
ComponentFactory构建器
这里很重要的一点是要记住:
我们的组件类型,用DynamicTypeBuilder构建,可能会有所不同——但只是它的模板不同(上面创建的)。组件的属性(输入、输出或某些受保护的属性)仍然相同。如果我们需要不同的属性,我们应该定义不同的模板和类型生成器组合
所以,我们触及了解的核心。Builder将1)创建ComponentType, 2)创建它的NgModule, 3)编译ComponentFactory, 4)缓存它以供以后重用。
我们需要接收的依赖项:
// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
@Injectable()
export class DynamicTypeBuilder {
// wee need Dynamic component builder
constructor(
protected compiler: JitCompiler
) {}
下面是如何获取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);
});
});
}
上面我们创建并缓存组件和模块。因为如果模板(实际上是真正的动态部分)是相同的..我们可以重复利用
这里有两个方法,它们代表了如何在运行时创建装饰类/类型的非常酷的方式。不仅是@Component,还有@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;
}
重要的是:
我们的组件动态类型不同,但只是根据模板不同。所以我们用这个事实来缓存它们。这真的非常重要。Angular2也会缓存这些..按类型。如果我们要为相同的模板字符串重新创建新类型…我们将开始产生内存泄漏。
宿主组件使用的ComponentFactory
最后一部分是一个组件,它承载动态组件的目标,例如<div #dynamicContentPlaceHolder></div>。我们获取它的引用,并使用ComponentFactory创建一个组件。简而言之,这里是该组件的所有部件(如果需要,在这里打开活塞)
让我们首先总结一下import语句:
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
) {}
...
我们只接收模板和组件构建器。下面是示例中需要的属性(详见注释)
// 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"
};
在这个简单的场景中,我们的宿主组件没有任何@Input。所以它不必对变化做出反应。但是尽管如此(并且为即将到来的更改做好准备),如果组件已经(第一次)初始化,我们需要引入一些标志。只有这样,我们才能开始施展魔法。
最后,我们将使用我们的组件构建器,它只是编译/缓存了componentfactory。我们的目标占位符将被要求用该工厂实例化组件。
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;
//...
});
}
小的扩展
此外,我们需要保持一个引用编译模板..能够正确地销毁()它,每当我们要改变它。
// 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;
}
}
done
差不多就是这样。不要忘记销毁任何动态构建的东西(ngOnDestroy)。此外,如果动态类型和模块的唯一区别是它们的模板,请确保缓存它们。
在这里查看所有的操作
要查看这篇文章的以前版本(例如RC5相关),请查看历史记录