我怎样才能 use/create 动态模板用 Angular 2.0 编译动态组件?

How can I use/create dynamic template to compile dynamic Component with 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)

并了解我可以使用其中任何一个

但问题是如何使用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>
   ...

等等 (number/date/reference editors 与 属性 类型不同,为某些用户跳过了一些属性...)。也就是说,这是一个示例,实际配置可能会生成更多不同和复杂的模板。

模板正在更改,所以我无法使用 ComponentFactoryResolver 并传递现有模板...我需要 Compiler.

的解决方案

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

相关

NOTE: to get solution for previous version, check the history of this post

这里讨论了类似的话题。我们需要使用 JitCompilerNgModule。在此处阅读有关 Angular2 中 NgModule 的更多信息:

简而言之

a working plunker/example(动态模板,动态组件类型,动态模块,JitCompiler,...在行动)

校长是:
1) 创建模板
2) 在缓存中找到 ComponentFactory - 转到 7)
3) - 创建 Component
4) - 创建 Module
5) - 编译Module
6) - return (并缓存以备后用) ComponentFactory
7) 使用 TargetComponentFactory 创建动态实例 Component

这是一个代码片段 (更多 here - 我们的自定义生成器 return 只是 built/cached ComponentFactory 和视图 Target 占位符用于创建 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++ & 运行时组件

下面this scenario的描述,我们将

  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. 的作业
  9. @Inputs分配给新实例(从INPUT切换到TEXTAREA编辑) , 消耗 @Outputs

NgModule

我们需要 NgModules。

While I would like to show a very simple example, in this case, I would need three modules (in fact 4 - but I do not count the AppModule). Please, take this rather than a simple snippet as a basis for a really solid dynamic component generator.

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

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

Where DYNAMIC_DIRECTIVES are extensible and are intended to hold all small parts used for our dynamic Component template/type. Check 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
            ], 
        };
    }
}

Check the usage of the forRoot() in the 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

的详细信息
entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
};

要创建一个 template,在这个 plunker 中,我们使用这个 simple/naive 构建器。

The real solution, a real template builder, is the place where your application can do a lot

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

一个ComponentFactory 生成器

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

our component type, build with our DynamicTypeBuilder, could differ - but only by its template (created above). Components' properties (inputs, outputs or some protected) are still same. If we need different properties, we should define different combination of Template and Type Builder

因此,我们触及了解决方案的核心。 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);
            });
    });
}

Above we create and cache both Component and Module. Because if the template (in fact the real dynamic part of that all) is the same.. we can reuse

这里有两个方法,它们代表了如何在运行时创建 decorated classes/types 的非常酷的方法。不仅 @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;
}

重要:

our component dynamic types differ, but just by template. So we use that fact to cache them. This is really very important. Angular2 will also cache these.. by the type. And if we would recreate for the same template strings new types... we will start to generate memory leaks.

ComponentFactory 被宿主组件使用

最后一块是一个组件,它承载我们动态组件的目标,例如<div #dynamicContentPlaceHolder></div>。我们获取对它的引用并使用 ComponentFactory 创建一个组件。简而言之,这里是该组件的所有部分(如果需要,打开plunker here

我们先总结一下导入语句:

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。所以它不必对变化做出反应。但尽管如此 (并为即将到来的变化做好准备) - 如果组件已经 (首先) 启动,我们需要引入一些标志.只有这样我们才能开始施展魔法。

最后我们将使用我们的组件生成器,它的只是compiled/cachedComponentFacotry。我们的目标占位符将被要求用那个工厂实例化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;
  }
}

完成

差不多就这些了。不要忘记 Destroy 任何动态构建的东西 (ngOnDestroy)。另外,如果唯一的区别是它们的模板,请确保缓存动态typesmodules

实际检查一下here

to see previous versions (e.g. RC5 related) of this post, check the history

我自己正在尝试查看如何将 RC4 更新为 RC5,因此我偶然发现了这个条目,动态组件创建的新方法对我来说仍然有点神秘,所以我不会在组件工厂解析器上提出任何建议.

但是,我可以建议在这种情况下使用更清晰的组件创建方法 - 只需在模板中使用 switch 即可根据某些条件创建字符串编辑器或文本编辑器,如下所示:

<form [ngSwitch]="useTextarea">
    <string-editor *ngSwitchCase="false" propertyName="'code'" 
                 [entity]="entity"></string-editor>
    <text-editor *ngSwitchCase="true" propertyName="'code'" 
                 [entity]="entity"></text-editor>
</form>

顺便说一下,[prop] 表达式中的“[”是有含义的,这表示一种数据绑定方式,因此你可以甚至应该省略那些,以防你知道你不需要绑定 属性 到变量。

我有一个简单的例子来展示如何做 angular 2 rc6 动态组件。

比如说,你有一个动态的html template = template1 想动态加载,先包装成component

@Component({template: template1})
class DynamicComponent {}

此处template1为html,可能包含ng2组件

从 rc6 开始,必须用 @NgModule 包装这个组件。 @NgModule,就像 anglarJS 1 中的模块一样,它解耦了 ng2 应用程序的不同部分,所以:

@Component({
  template: template1,

})
class DynamicComponent {

}
@NgModule({
  imports: [BrowserModule,RouterModule],
  declarations: [DynamicComponent]
})
class DynamicModule { }

(这里导入 RouterModule,在我的示例中,我的 html 中有一些路由组件,您稍后会看到)

现在您可以将 DynamicModule 编译为: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

而且我们需要把上面的放在app.moudule.ts中才能加载,请看我的app.moudle.ts。 有关更多和完整的详细信息,请检查: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts 和 app.moudle.ts

并查看演示:http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

编辑(2017 年 8 月 26 日):下面的解决方案适用于 Angular2 和 4。我已经更新它以包含一个模板变量和单击处理程序并使用 Angular 4.3.
对其进行测试 对于Angular4,接受 is a much better solution. But right now it does not support inputs & outputs yet. If [this PR](https://github.com/angular/angular/pull/15362]中描述的ngComponentOutlet,可以通过create事件返回的组件实例。
ng-dynamic-component 可能是最好和最简单的解决方案,但我还没有测试过。

@Long Field 的回答很到位!这是另一个(同步)示例:

import {Compiler, Component, NgModule, OnInit, ViewChild,
  ViewContainerRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `<h1>Dynamic template:</h1>
             <div #container></div>`
})
export class App implements OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private compiler: Compiler) {}

  ngOnInit() {
    this.addComponent(
      `<h4 (click)="increaseCounter()">
        Click to increase: {{counter}}
      `enter code here` </h4>`,
      {
        counter: 1,
        increaseCounter: function () {
          this.counter++;
        }
      }
    );
  }

  private addComponent(template: string, properties?: any = {}) {
    @Component({template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
    Object.assign(component.instance, properties);
    // If properties are changed at a later stage, the change detection
    // may need to be triggered manually:
    // component.changeDetectorRef.detectChanges();
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

住在http://plnkr.co/edit/fdP9Oc

我决定把我学到的所有东西压缩成一个文件。 特别是与 RC5 之前相比,这里有很多东西可以吸收。请注意,此源文件包含 AppModule 和 AppComponent。

import {
  Component, Input, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
  OnInit, ViewChild
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

@Component({
  selector: 'app-dynamic',
  template: '<h4>Dynamic Components</h4><br>'
})
export class DynamicComponentRenderer implements OnInit {

  factory: ModuleWithComponentFactories<DynamicModule>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }

  ngOnInit() {
    if (!this.factory) {
      const dynamicComponents = {
        sayName1: {comp: SayNameComponent, inputs: {name: 'Andrew Wiles'}},
        sayAge1: {comp: SayAgeComponent, inputs: {age: 30}},
        sayName2: {comp: SayNameComponent, inputs: {name: 'Richard Taylor'}},
        sayAge2: {comp: SayAgeComponent, inputs: {age: 25}}};
      this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
        .then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
          this.factory = moduleWithComponentFactories;
          Object.keys(dynamicComponents).forEach(k => {
            this.add(dynamicComponents[k]);
          })
        });
    }
  }

  addNewName(value: string) {
    this.add({comp: SayNameComponent, inputs: {name: value}})
  }

  addNewAge(value: number) {
    this.add({comp: SayAgeComponent, inputs: {age: value}})
  }

  add(comp: any) {
    const compFactory = this.factory.componentFactories.find(x => x.componentType === comp.comp);
    // If we don't want to hold a reference to the component type, we can also say: const compFactory = this.factory.componentFactories.find(x => x.selector === 'my-component-selector');
    const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
    const cmpRef = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
    Object.keys(comp.inputs).forEach(i => cmpRef.instance[i] = comp.inputs[i]);
  }
}

@Component({
  selector: 'app-age',
  template: '<div>My age is {{age}}!</div>'
})
class SayAgeComponent {
  @Input() public age: number;
};

@Component({
  selector: 'app-name',
  template: '<div>My name is {{name}}!</div>'
})
class SayNameComponent {
  @Input() public name: string;
};

@NgModule({
  imports: [BrowserModule],
  declarations: [SayAgeComponent, SayNameComponent]
})
class DynamicModule {}

@Component({
  selector: 'app-root',
  template: `
        <h3>{{message}}</h3>
        <app-dynamic #ad></app-dynamic>
        <br>
        <input #name type="text" placeholder="name">
        <button (click)="ad.addNewName(name.value)">Add Name</button>
        <br>
        <input #age type="number" placeholder="age">
        <button (click)="ad.addNewAge(age.value)">Add Age</button>
    `,
})
export class AppComponent {
  message = 'this is app component';
  @ViewChild(DynamicComponentRenderer) dcr;

}

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, DynamicComponentRenderer],
  bootstrap: [AppComponent]
})
export class AppModule {}`

我想在 Radim 的这个非常出色的 post 之上添加一些细节。

我采用了这个解决方案并对其进行了一些研究,很快 运行 遇到了一些限制。我只会概述这些,然后给出解决方案。

  • 首先,我无法在 动态细节(基本上是相互嵌套动态 UI)。
  • 下一个问题是我想在里面渲染一个动态细节 解决方案中可用的部分之一。那是 初始解决方案也不可能。
  • 最后,无法在字符串编辑器等动态部分使用模板 URL。

我基于这个 post 提出了另一个问题,关于如何实现这些限制,可以在这里找到:

如果您 运行 遇到与我相同的问题,我将概述这些限制的答案,因为这会使解决方案更加灵活。将初始的 plunker 也用它更新会很棒。

要使动态细节相互嵌套,您需要在 type.builder.ts[ 的导入语句中添加 DynamicModule.forRoot() =25=]

protected createComponentModule (componentType: any) {
    @NgModule({
    imports: [
        PartsModule, 
        DynamicModule.forRoot() //this line here
    ],
    declarations: [
        componentType
    ],
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
}

此外,无法在字符串编辑器或文本编辑器的其中一个部分中使用 <dynamic-detail>

要启用它,您需要更改 parts.module.tsdynamic.module.ts

parts.module.ts 中您需要在 DYNAMIC_DIRECTIVES

中添加 DynamicDetail
export const DYNAMIC_DIRECTIVES = [
   forwardRef(() => StringEditor),
   forwardRef(() => TextEditor),
   DynamicDetail
];

同样在 dynamic.module.ts 中,您必须删除 dynamicDetail,因为它们现在是部件的一部分

@NgModule({
   imports:      [ PartsModule ],
   exports:      [ PartsModule],
})

可以在这里找到一个有效的修改插件:http://plnkr.co/edit/UYnQHF?p=preview(我没有解决这个问题,我只是一个信使:-D)

最后,无法在动态组件上创建的部分中使用 templateurls。一个解决方案(或解决方法。我不确定它是 angular 错误还是框架使用不当)是在构造函数中创建一个编译器而不是注入它。

    private _compiler;

    constructor(protected compiler: RuntimeCompiler) {
        const compilerFactory : CompilerFactory =
        platformBrowserDynamic().injector.get(CompilerFactory);
        this._compiler = compilerFactory.createCompiler([]);
    }

然后使用_compiler编译,templateUrls也开启。

return new Promise((resolve) => {
        this._compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                let _ = window["_"];
                factory = _.find(moduleWithFactories.componentFactories, { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

希望这对其他人有帮助!

此致 莫腾

继 Radmin 的出色回答之后,使用 angular-cli 版本 1.0.0-beta.22 及更高版本的每个人都需要进行一些调整。

COMPILER_PROVIDERS无法再导入(详情见angular-cli GitHub)。

所以解决方法是根本不在 providers 部分使用 COMPILER_PROVIDERSJitCompiler,而是使用 '@angular/compiler 中的 JitCompilerFactory ' 而不是在类型构建器中这样 class:

private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

如您所见,它是不可注入的,因此与 DI 没有依赖关系。此解决方案也适用于不使用 angular-cli.

的项目

我一定是迟到了,none 这里的解决方案似乎对我有帮助 - 太乱了,感觉像是一个太多的解决方法。

我最后做的是使用 Angular 4.0.0-beta.6ngComponentOutlet

这给了我所有写在动态组件文件中的最短、最简单的解决方案。

  • 这是一个简单的示例,它只接收文本并将其放置在模板中,但显然您可以根据需要进行更改:
import {
  Component, OnInit, Input, NgModule, NgModuleFactory, Compiler
} from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<ng-container *ngComponentOutlet="dynamicComponent;
                            ngModuleFactory: dynamicModule;"></ng-container>`,
  styleUrls: ['my.component.css']
})
export class MyComponent implements OnInit {
  dynamicComponent;
  dynamicModule: NgModuleFactory<any>;

  @Input()
  text: string;

  constructor(private compiler: Compiler) {
  }

  ngOnInit() {
    this.dynamicComponent = this.createNewComponent(this.text);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));
  }

  protected createComponentModule (componentType: any) {
    @NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
  }

  protected createNewComponent (text:string) {
    let template = `dynamically created template with text: ${text}`;

    @Component({
      selector: 'dynamic-component',
      template: template
    })
    class DynamicComponent implements OnInit{
       text: any;

       ngOnInit() {
       this.text = text;
       }
    }
    return DynamicComponent;
  }
}
  • 简短说明:
    1. my-component - 渲染动态组件的组件
    2. DynamicComponent - 要动态构建的组件,它在 my-component
    3. 内渲染

不要忘记将所有 angular 库升级到 ^Angular 4.0.0

希望对您有所帮助,祝您好运!

更新

也适用于 angular 5.

在 Angular 2 最终版本中解决了这个问题,只需使用 dynamicComponent directive from ng-dynamic

用法:

<div *dynamicComponent="template; context: {text: text};"></div>

其中模板是您的动态模板,上下文可以设置为您希望模板绑定到的任何动态数据模型。

在 Ophir Stern 的回答之上,这里有一个变体可以在 Angular 4 中与 AoT 一起工作。我唯一的问题是我不能将任何服务注入 DynamicComponent,但我可以活下去

注意:我还没有用 Angular 5.

测试过
import { Component, OnInit, Input, NgModule, NgModuleFactory, Compiler, EventEmitter, Output } from '@angular/core';
import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

type Bindings = {
  [key: string]: any;
};

@Component({
  selector: 'app-compile',
  template: `
    <div *ngIf="dynamicComponent && dynamicModule">
      <ng-container *ngComponentOutlet="dynamicComponent; ngModuleFactory: dynamicModule;">
      </ng-container>
    </div>
  `,
  styleUrls: ['./compile.component.scss'],
  providers: [{provide: Compiler, useFactory: createJitCompiler}]
})
export class CompileComponent implements OnInit {

  public dynamicComponent: any;
  public dynamicModule: NgModuleFactory<any>;

  @Input()
  public bindings: Bindings = {};
  @Input()
  public template: string = '';

  constructor(private compiler: Compiler) { }

  public ngOnInit() {

    try {
      this.loadDynamicContent();
    } catch (err) {
      console.log('Error during template parsing: ', err);
    }

  }

  private loadDynamicContent(): void {

    this.dynamicComponent = this.createNewComponent(this.template, this.bindings);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));

  }

  private createComponentModule(componentType: any): any {

    const runtimeComponentModule = NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })(class RuntimeComponentModule { });

    return runtimeComponentModule;

  }

  private createNewComponent(template: string, bindings: Bindings): any {

    const dynamicComponent = Component({
      selector: 'app-dynamic-component',
      template: template
    })(class DynamicComponent implements OnInit {

      public bindings: Bindings;

      constructor() { }

      public ngOnInit() {
        this.bindings = bindings;
      }

    });

    return dynamicComponent;

  }

}

希望对您有所帮助。

干杯!

这是从服务器生成的动态表单控件的示例。

https://stackblitz.com/edit/angular-t3mmg6

此示例是添加组件中的动态表单控件(您可以在此处从服务器获取表单控件)。如果您看到 addcomponent 方法,您就可以看到 Forms 控件。在此示例中,我没有使用 angular material,但它有效(我使用的是 @work)。这是 angular 6 的目标,但适用于所有以前的版本。

需要为 AngularVersion 5 及更高版本添加 JITComplierFactory。

谢谢

维杰

2019年6月答案

好消息!看来 @angular/cdk package now has first-class support for portals!

截至撰写本文时,我没有发现上述官方文档特别有用(特别是关于向动态组件发送数据和从动态组件接收事件)。总之,您将需要:

步骤 1) 更新您的 AppModule

@angular/cdk/portal 包中导入 PortalModule 并在 entryComponents

中注册您的动态组件
@NgModule({
  declarations: [ ..., AppComponent, MyDynamicComponent, ... ]
  imports:      [ ..., PortalModule, ... ],
  entryComponents: [ ..., MyDynamicComponent, ... ]
})
export class AppModule { }

步骤 2. 选项 A:如果您不需要向动态组件传递数据和从动态组件接收事件:

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add child component</button>
    <ng-template [cdkPortalOutlet]="myPortal"></ng-template>
  `
})
export class AppComponent  {
  myPortal: ComponentPortal<any>;
  onClickAddChild() {
    this.myPortal = new ComponentPortal(MyDynamicComponent);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child.</p>`
})
export class MyDynamicComponent{
}

See it in action

步骤 2. 选项 B:如果您确实需要将数据传递到动态组件并从中接收事件

// A bit of boilerplate here. Recommend putting this function in a utils 
// file in order to keep your component code a little cleaner.
function createDomPortalHost(elRef: ElementRef, injector: Injector) {
  return new DomPortalHost(
    elRef.nativeElement,
    injector.get(ComponentFactoryResolver),
    injector.get(ApplicationRef),
    injector
  );
}

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add random child component</button>
    <div #portalHost></div>
  `
})
export class AppComponent {

  portalHost: DomPortalHost;
  @ViewChild('portalHost') elRef: ElementRef;

  constructor(readonly injector: Injector) {
  }

  ngOnInit() {
    this.portalHost = createDomPortalHost(this.elRef, this.injector);
  }

  onClickAddChild() {
    const myPortal = new ComponentPortal(MyDynamicComponent);
    const componentRef = this.portalHost.attach(myPortal);
    setTimeout(() => componentRef.instance.myInput 
      = '> This is data passed from AppComponent <', 1000);
    // ... if we had an output called 'myOutput' in a child component, 
    // this is how we would receive events...
    // this.componentRef.instance.myOutput.subscribe(() => ...);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child. <strong>{{myInput}}</strong></p>`
})
export class MyDynamicComponent {
  @Input() myInput = '';
}

See it in action

在 angular 7.x 中,我为此使用了 angular 元素。

  1. 安装@angular-elements npm i @angular/elements -s

  2. 创建附件服务。

import { Injectable, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { IStringAnyMap } from 'src/app/core/models';
import { AppUserIconComponent } from 'src/app/shared';

const COMPONENTS = {
  'user-icon': AppUserIconComponent
};

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentsService {
  constructor(private injector: Injector) {

  }

  public register(): void {
    Object.entries(COMPONENTS).forEach(([key, component]: [string, any]) => {
      const CustomElement = createCustomElement(component, { injector: this.injector });
      customElements.define(key, CustomElement);
    });
  }

  public create(tagName: string, data: IStringAnyMap = {}): HTMLElement {
    const customEl = document.createElement(tagName);

    Object.entries(data).forEach(([key, value]: [string, any]) => {
      customEl[key] = value;
    });

    return customEl;
  }
}

请注意,您的自定义元素标签必须与 angular 组件选择器不同。 在 AppUserIconComponent 中:

...
selector: app-user-icon
...

在本例中,我使用了自定义标签名称 "user-icon"。

  1. 那你必须在AppComponent中调用register:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. 现在您可以在代码的任何地方像这样使用它:
dynamicComponents.create('user-icon', {user:{...}});

或者像这样:

const html = `<div class="wrapper"><user-icon class="user-icon" user='${JSON.stringify(rec.user)}'></user-icon></div>`;

this.content = this.domSanitizer.bypassSecurityTrustHtml(html);

(在模板中):

<div class="comment-item d-flex" [innerHTML]="content"></div>

请注意,在第二种情况下,您必须使用 JSON.stringify 传递对象,然后再次解析它。我找不到更好的解决方案。

对于这种特殊情况,使用指令动态创建组件似乎是更好的选择。 示例:

在HTML你想创建组件的地方

<ng-container dynamicComponentDirective [someConfig]="someConfig"></ng-container>

我会按照以下方式处理和设计指令。

const components: {[type: string]: Type<YourConfig>} = {
    text : TextEditorComponent,
    numeric: NumericComponent,
    string: StringEditorComponent,
    date: DateComponent,
    ........
    .........
};

@Directive({
    selector: '[dynamicComponentDirective]'
})
export class DynamicComponentDirective implements YourConfig, OnChanges, OnInit {
    @Input() yourConfig: Define your config here //;
    component: ComponentRef<YourConfig>;

    constructor(
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef
    ) {}

    ngOnChanges() {
        if (this.component) {
            this.component.instance.config = this.config;
            // config is your config, what evermeta data you want to pass to the component created.
        }
    }

    ngOnInit() {
        if (!components[this.config.type]) {
            const supportedTypes = Object.keys(components).join(', ');
            console.error(`Trying to use an unsupported type ${this.config.type} Supported types: ${supportedTypes}`);
        }

        const component = this.resolver.resolveComponentFactory<yourConfig>(components[this.config.type]);
        this.component = this.container.createComponent(component);
        this.component.instance.config = this.config;
    }
}

因此,在您的组件中,文本、字符串、日期等等 - 无论您在 ng-container 元素中的 HTML 中传递什么配置,都将可用。

配置 yourConfig 可以相同并定义您的元数据。

根据您的配置或输入类型,指令应根据支持的类型采取相应的行动,它将呈现适当的组件。如果不是,它将记录一个错误。

如果您只需要解析动态字符串并通过选择器加载组件,您可能还会发现 ngx-dynamic-hooks 库很有用。我最初创建它是作为个人项目的一部分,但没有看到任何类似的东西,所以我对其进行了一些润色并使其成为 public.

一些花絮:

  • 您可以通过选择器(或您选择的任何其他模式!)将任何组件加载到动态字符串中!
  • 可以像在普通模板中一样设置输入和输出
  • 组件可以无限制嵌套
  • 您可以将实时数据从父组件传递到动态加载的组件中(甚至用它来绑定inputs/outputs)
  • 您可以控制哪些组件可以加载到每个插座中,甚至可以 inputs/outputs 给它们
  • 该库使用 Angular 的 built-in DOMSanitizer,即使输入可能不安全也能安全使用。

值得注意的是,它 而不是 依赖于 runtime-compiler ,就像这里的其他一些回复一样。因此,您不能使用模板语法。另一方面,这意味着它可以在 JiT 和 AoT-modes 以及 Ivy 和旧模板引擎中工作,并且通常使用起来更加安全。

查看实际效果 in this Stackblitz

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

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

基本上是因为 Angular 不允许您定义组件: 模板:{变量}

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