如何使用/创建动态模板来使用 Angular 2.0 编译动态组件?

新手上路,请多包涵

我想动态创建一个模板。这应该用于在运行时构建 ComponentType 并将其放置 (甚至替换) 托管组件内部的某个位置。

在 RC4 之前,我使用的是 ComponentResolver ,但是使用 RC5 我收到以下消息:

 ComponentResolver is deprecated for dynamic compilation.
Use ComponentFactoryResolver together with @NgModule/@Component.entryComponents or ANALYZE_FOR_ENTRY_COMPONENTS provider instead.
For runtime compile only, you can also use Compiler.compileComponentSync/Async.

我找到了这个文档( Angular 2 Synchronous Dynamic Component Creation

并了解我可以使用

  • 一种动态的 ngIfComponentFactoryResolver 。如果我通过 @Component({entryComponents: [comp1, comp2], ...}) 内部的已知组件 - 我可以使用 .resolveComponentFactory(componentToRender);
  • 真正的运行时编译,用 Compiler

但问题是如何使用 Compiler ?上面的注释说我应该打电话: Compiler.compileComponentSync/Async - 那怎么办?

例如。我想为一种设置创建 (基于一些配置条件) 这种模板

<form>
   <string-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></string-editor>
   <string-editor
     [propertyName]="'description'"
     [entity]="entity"
   ></string-editor>
   ...

在另一种情况下,这个 string-editor 被替换为 text-editor

 <form>
   <text-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></text-editor>
   ...

依此类推 (不同的编号/日期/参考 editors 按属性类型,为某些用户跳过了一些属性…) 。即这是一个示例,实际配置可能会生成更多不同和复杂的模板。

模板正在更改,因此我无法使用 ComponentFactoryResolver 并传递现有模板…我需要 Compiler 的解决方案。

原文由 Radim Köhler 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 524
2 个回答

编辑 - 与 2.3.0 相关(2016-12-07)

注意:要获得以前版本的解决方案,请查看此帖子的历史记录

此处讨论了类似的主题 Angular 2 中的 $compile 等效项。我们需要使用 JitCompilerNgModule 。在 Angular2 中阅读更多关于 NgModule 的信息:

简而言之

一个有效的插件/示例 (动态模板,动态组件类型,动态模块, JitCompiler ,……在行动)

主要是:

1) 创建模板

2) 在缓存中找到 ComponentFactory - 转到 7)

    • 创建 Component
    • 创建 Module
    • 编译 Module
    • 返回(并缓存供以后使用) ComponentFactory

7) 使用 TargetComponentFactory 创建一个动态实例 Component

这是一个代码片段 这里 有更多) - 我们的自定义生成器正在返回刚刚构建/缓存的 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

观察一个 plunker 并回来阅读详细信息,以防某些片段需要更多解释

.

详解——Angular2 RC6++ & 运行时组件

下面描述 这个场景,我们将

  1. 创建一个模块 PartsModule:NgModule (小件支架)
  2. 创建另一个模块 DynamicModule:NgModule ,它将包含我们的动态组件 (和引用 PartsModule 动态)
  3. 创建动态模板 (简单方法)
  4. 创建新的 Component 类型 (仅当模板已更改时)
  5. 创建新的 RuntimeModule:NgModule 。该模块将包含先前创建的 Component 类型
  6. 调用 JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) 得到 ComponentFactory
  7. 创建 DynamicComponent 的实例 - View Target 占位符的作业和 ComponentFactory
  8. @Inputs 分配给 新实例 (从 INPUT 切换到 TEXTAREA 编辑) ,使用 @Outputs

模块

我们需要一个 NgModule s。

虽然我想展示一个非常简单的示例,但在这种情况下,我需要三个模块 (实际上是 4 个 - 但我不计算 AppModule) 。请把这个 而不是一个简单的片段 作为一个真正可靠的动态组件生成器的基础。

所有小组件都将有 一个 模块,例如 string-editor , text-editor ( date-editor , number-editor ) …

 @NgModule({
  imports:      [
      CommonModule,
      FormsModule
  ],
  declarations: [
      DYNAMIC_DIRECTIVES
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule
  ]
})
export class PartsModule { }

其中 DYNAMIC_DIRECTIVES 是可扩展的,旨在容纳用于我们的动态组件模板/类型的所有小部件。检查 app/parts/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
            ],
        };
    }
}

forRoot() 中检查 AppModule 的使用情况

最后,我们将需要一个临时的运行时模块.. 但这将在以后创建,作为 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 的信息:

模板 生成器

在我们的示例中,我们将处理此类 实体 的详细信息

entity = {
    code: "ABC123",
    description: "A description of this Entity"
};

要创建一个 template ,在这个 plunker 中,我们使用这个简单/幼稚的构建器。

真正的解决方案,真正的模板构建器,是您的应用程序可以做很多事情的地方

// 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>";
    }
}

这里的一个技巧是 - 它构建了一个使用一组已知属性的模板,例如 entity 。这样的属性(-ies)必须是我们接下来要创建的动态组件的一部分。

为了使它更容易一点,我们可以使用一个接口来定义我们的模板构建器可以使用的属性。这将由我们的动态组件类型实现。

 export interface IHaveDynamicData {
    public entity: any;
    ...
}

A ComponentFactory 建造者

这里非常重要的是要记住:

我们的组件类型,使用我们的 DynamicTypeBuilder 构建,可能会有所不同 - 但仅限于其模板 (在上面创建) 。组件的属性 (输入、输出或某些 受保护的)仍然相同。 如果我们需要不同的属性,我们应该定义不同的模板和类型生成器组合

因此,我们正在触及解决方案的核心。生成器将 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);
            });
    });
}

上面我们创建并 缓存ComponentModule 。因为如果模板 (实际上是真正的动态部分) 是相同的..我们可以重用

这里有两种方法,它们代表了如何在运行时创建 装饰 类/类型的非常酷的方法。不仅 @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 创建一个组件。简而言之,这里是该组件的所有部分 (如果需要, 请在此处打开 plunker

我们先总结一下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 。所以它不必对变化做出反应。但是尽管有这个事实 (并为即将发生的变化做好准备) - 如果组件已经 (首先) 启动,我们需要引入一些标志。只有这样我们才能开始魔术。

最后,我们将使用我们的组件构建器,以及它 刚刚编译/缓存ComponentFacotry 。我们的 Target 占位符 将被要求用该工厂实例化 Component

 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;
        //...
    });
}

小扩展

此外,我们需要保留对已编译模板的引用.. 以便能够正确地 destroy() 它,无论何时我们将更改它。

 // 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;
  }
}

完毕

差不多就是这样。不要忘记 销毁 任何动态构建的东西 (ngOnDestroy) 。此外,如果唯一的区别是它们的模板,请务必 缓存 动态 typesmodules

这里 检查一切

要查看此帖子的先前版本 (例如 RC5 相关) ,请查看 历史记录

原文由 Radim Köhler 发布,翻译遵循 CC BY-SA 4.0 许可协议

在 2021 年,Angular 仍然无法使用动态 HTML(动态加载 html 模板)创建组件,只是为了节省您的时间。

即使有很多投票赞成的解决方案和接受的解决方案,但至少目前它们都不适用于生产/AOT 中的最新版本。

基本上是因为 Angular 不允许您使用 : template: {variable} 定义组件

正如 Angular 团队所说,他们不会支持这种方法!!请找到这个以供参考 https://github.com/angular/angular/issues/15275

原文由 Mosta 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进