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


196

我想动态创建一个模板。这应该用于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同步动态组件创建

并了解我可以使用

  • 一种动态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


因为我找到的解决方案真是太好了,所以我希望每个发现此问题的人都可以看看我的答案,该答案目前在最底层。:)
理查德·霍尔茨


这是每个答案都存在的问题$compile,这些方法实际上不能做什么—我正在创建一个应用程序,我只想编译通过第三方页面和ajax调用传入的HTML。我无法从页面中删除HTML并将其放在自己的模板中。感叹
Augie Gardner

@AugieGardner这是设计上无法实现的原因。Angular不会因某些人的错误体系结构决策或旧系统而过错。如果您想解析现有的HTML代码,则可以自由使用其他框架,因为Angular与WebComponents可以很好地兼容。设置清晰的边界以引导成群的缺乏经验的程序员,比允许对少数旧系统进行肮脏的黑客攻击更为重要。
菲尔(Phil)

Answers:


162

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

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

这里讨论了类似的话题Angular 2中的$ compile等效项。我们需要使用JitCompilerNgModuleNgModule在此处阅读有关Angular2的更多信息:

简而言之

一个有用的插件/示例 (动态模板,动态组件类型,动态模块JitCompiler,...正在运行)

原理是:
1)创建模板
2)ComponentFactory在缓存中查找- 转到7)
3)-创建Component
4)-创建Module
5)-编译Module
6)-返回(以及缓存供以后使用)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

观察一下插棒,然后再阅读详细信息,以防某些片段需要更多说明

详细说明-Angular2 RC6 ++和运行时组件

下面对此场景进行说明,我们将

  1. 创建一个模块PartsModule:NgModule (小块的持有人)
  2. 创建另一个模块DynamicModule:NgModule,其中将包含我们的动态组件(并PartsModule动态引用)
  3. 创建动态模板(简单方法)
  4. 创建新Component类型(仅当模板已更改时)
  5. 创造新的RuntimeModule:NgModule。该模块将包含先前创建的Component类型
  6. 致电JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)获取ComponentFactory
  7. 创建DynamicComponent“查看目标”占位符-作业的实例,然后ComponentFactory
  8. 分配@Inputs新实例 (从切换INPUTTEXTAREA编辑),使用@Outputs

Ng模块

我们需要一个NgModules。

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

将有一个模块,所有的小部件,例如string-editortext-editor date-editornumber-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 - 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。这样的属性必须是动态组件的一部分,我们接下来将创建它。

为了使操作更简单,我们可以使用一个界面来定义属性,模板构建器可以使用这些属性。这将通过我们的动态Component类型实现。

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

一个ComponentFactory建设者

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

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

因此,我们正在触及解决方案的核心。生成器将1)创建ComponentType2)创建其NgModule3)编译ComponentFactory4)将其缓存以备后用。

我们需要接收的依赖项:

// 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创建一个组件。简而言之,这是该组件的所有组件(如果需要,请在此处打开塞子

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

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。我们的目标占位符将被要求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相关),请查看历史记录


50
这看起来像是一个如此复杂的解决方案,不推荐使用的解决方案非常简单明了,还有其他方法吗?
tibbus

3
我认为与@tibbus的方式相同:这种方式比不推荐使用的代码要复杂得多。不过,谢谢您的回答。
Lucio Mollinedo'9

5
@ribsies感谢您的来信。让我澄清一下。许多其他答案试图使其变得简单。但是我试图解释它,并在不符合实际使用的情况下显示它。我们将需要缓存内容,我们必须在重新创建时调用destroy等。因此,type.builder.ts正如您所指出的那样,虽然动态构建的魅力确实存在,但我希望任何用户都可以理解如何将它们全部放入上下文...希望它可能有用;)
RadimKöhler16年

7
@RadimKöhler-我已经尝试过此示例。它没有AOT就可以工作。但是,当我尝试使用AOT运行此文件时,它显示错误“找不到RuntimeComponentModule的NgModule元数据”。您能帮我解决这个错误吗?
Trusha

4
答案本身就是完美的!但是对于现实生活中的应用并不可行。角度小组应在框架中为此提供解决方案,因为这是业务应用程序中的常见要求。如果不是,则必须询问Angular 2是否是适合业务应用程序的平台。
卡尔

58

编辑(26/08/2017):以下解决方案与Angular2和4配合使用。我已对其进行了更新,以包含模板变量和单击处理程序,并使用Angular 4.3对其进行了测试。
对于Angular4,如Ophir的答案中所述ngComponentOutlet 是更好的解决方案。但是现在它还不支持输入和输出。如果接受[this PR](https://github.com/angular/angular/pull/15362],则可以通过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


3
我想说的是,这是一个示例,该示例显示如何编写尽可能少的代码来完成与我的答案stackoverflow.com/a/38888009/1679310相同的操作。以防万一,当条件改变时,它应该是有用的情况(主要是重新生成模板) ...带的简单ngAfterViewInit调用const template将不起作用。但是,如果您的任务是减少上述详细描述的方法(创建模板,创建组件,创建模块,对其进行编译,创建工厂..创建实例) ……您可能做到了
RadimKöhler16

感谢您的解决方案:虽然我在加载templateUrl和样式时遇到问题,但出现以下错误:没有提供ResourceLoader实现。无法读取url localhost:3000 / app / pages / pages_common.css,知道我所缺少的吗?
Gerardlamo

是否可以使用控件等网格中的单元格数据编译html模板?plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview在此plunker中,如何编译并在最后一栏中显示图像?有帮助吗?
Karthick '16

1
@monnef,你是对的。我没有检查控制台日志。我已经调整了代码以将组件添加到ngOnInit中,而不是ngAfterViewInit挂钩中,因为前者在更改检测之前被触发,后者更改检测之后被触发。(请参阅github.com/angular/angular/issues/10131和类似的线程。)
Rene Hamburger,

1
简洁明了。通过开发人员的浏览器提供服务时按预期方式工作。但这对AOT有用吗?编译后在PROD中运行该应用程序时,尝试进行组件编译时出现“错误:运行时编译器未加载”。(顺便说一句,我正在使用Ionic 3.5)
mymo

52

我一定是来晚了,这里的所有解决方案似乎都对我没有帮助-太乱了,感觉像是太多的解决方法。

我落得这样做使用是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内部渲染

不要忘记将所有的角度库都升级到^ Angular 4.0.0

希望这有帮助,祝你好运!

更新

也适用于角度5。


3
这对我使用Angular4来说非常有用。我唯一要做的调整是能够为动态创建的RuntimeComponentModule指定导入模块。
拉胡尔·帕特尔

8
这是一个从Angular快速入门开始的简单示例:embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel

5
此解决方案是否可以与“ ng build --prod”一起使用?看来编译器类和AoT不能一起容纳atm。
皮埃尔·查瓦罗什

2
@OphirStern我还发现该方法在Angular 5中效果很好,但没有--prod build标志。
TaeKwonJ​​oe '18

2
我使用JitCompilerFactory以角度5(5.2.8)进行了测试,并使用--prod标志不起作用!有没有人有办法解决吗?(没有--prod标志的BTW JitCompilerFactory可以正常工作)
Frank

20

2019年6月答案

好消息!看来@ angular / cdk软件包现在已经对门户提供了一流的支持!

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

步骤1)更新您的 AppModule

PortalModule@angular/cdk/portal包中导入并在其中注册您的动态组件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{
}

实际观看

步骤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 = '';
}

实际观看


1
杜德,你刚刚钉了。这将引起关注。在我需要做一个之前,我不敢相信在Angular中添加一个简单的动态组件是多么的困难。这就像进行重置并返回到JQuery之前的时间一样。
Gi1ber7 '19

2
@ Gi1ber7我知道吗?为什么要花这么长时间?
斯蒂芬·保罗,

1
好的方法,但是您知道如何将参数传递给ChildComponent吗?
Snook

1
@Snook这可能会回答你的问题stackoverflow.com/questions/47469844/...
斯蒂芬·保罗

4
@StephenPaul这种Portal方法与ngTemplateOutletngComponentOutlet有何不同?🤔
格伦·穆罕默德·

18

我决定将学到的所有内容压缩到一个文件中。特别是与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 {}`

10

我有一个简单的示例来演示如何做角度2 rc6动态分量。

假设您有一个动态的html template = template1并想动态加载,首先要包装到组件中

@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


3
因此,您已经声明了module1,module2,module3。并且,如果您需要其他“动态”模板内容,则需要创建一个moudle4(module4.ts)形式的定义(文件),对吗?如果是,那似乎不是动态的。它是静态的,不是吗?还是我想念什么?
RadimKöhler'16

在上面的“ template1”是html的字符串中,您可以在其中添加任何内容,我们将其称为动态模板,因为这个问题在问
Long Field

6

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

  1. 在@ angular / elements -s中安装@ angular-elements npm

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

请注意,自定义元素标签必须与角度分量选择器不同。在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的对象,然后再次解析它。我找不到更好的解决方案。


有趣的方法,但是您将需要在tsconfig.json中将es2015(因此不支持IE11)作为目标,否则它将在document.createElement(tagName);
Snook

嗨,正如您提到的处理输入的方法一样,子组件的输出也可以这样处理吗?
Mustahsan '19

5

只需使用ng-dynamicdynamicComponent指令即可在Angular 2 Final版本中解决此问题。

用法:

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

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


在编写AOT的Angular 5时,不支持此功能,因为捆绑包中未包含JIT编译器。没有AOT,它就像一个魅力:)
Richard Houltz '18

这仍然适用于有角度的7+吗?
卡洛斯E

4

我想在Radim的这篇非常出色的文章的基础上添加一些细节。

我采用了此解决方案并对其进行了一段时间的研究,很快遇到了一些限制。我将概述这些内容,然后再给出解决方案。

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

我根据这篇文章提出了另一个问题,关于如何实现这些限制,可以在这里找到:

angular2中的递归动态模板编译

如果您遇到与我相同的问题,我将概述这些限制的答案,因为这会使解决方案更加灵活。还要对原始的插件进行更新。

要在彼此之间嵌套动态详细信息,您需要在type.builder.ts的import语句中添加DynamicModule.forRoot()。

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你需要添加DynamicDetailDYNAMIC_DIRECTIVES

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。一个解决方案(或解决方法。我不确定这是一个有角度的错误还是对框架的错误使用)是在构造函数中创建一个编译器,而不是注入它。

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

希望这对别人有帮助!

最好的问候莫滕


4

遵循Radmin的出色回答,对于使用1.70-beta.22及更高版本的angular-cli版本的每个人,都需要进行一些调整。

COMPILER_PROVIDERS不能再导入(有关详细信息,请参见angular-cli GitHub)。

因此,解决方法是根本不使用COMPILER_PROVIDERSJitCompiler在本providers节中,而是JitCompilerFactory在类型生成器类中使用“ @ angular / compiler”代替:

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

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


1
但是,感谢您的建议,我遇到了“找不到“ DynamicHtmlModule”的NgModule元数据”。我的实现是基于stackoverflow.com/questions/40060498/...
Cybey

2
任何人都可以使用AOT示例运行JitCompiletFactory?我有与@Cybey相同的错误
user2771738


2

我本人试图查看如何将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]表达式中的“ [”具有某种含义,它表示数据绑定的一种方式,因此,即使您知道不需要将属性绑定到变量,也可以甚至应该省略这些绑定。


1
如果switch/ case包含很少的决定,那将是一种方法。但是,请想象一下,生成的模板可能确实很大...并且每个实体都不同,安全性不同,实体状态不同,每种属性类型(数字,日期,参考...编辑器)...在这种情况下,在html模板中解决此问题ngSwitch会创建一个非常大的html文件。
RadimKöhler'16

哦,我同意你的看法。我现在在这里有这种情况,因为我正在尝试加载应用程序的主要组件,而在编译之前不知道要显示的特定类。尽管这种特殊情况不需要动态创建组件。
2014年

1

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

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

此示例是动态Form控件位于add组件中(您可以在此处从服务器获取Formcontrols)。如果您看到addcomponent方法,则可以看到窗体控件。在此示例中,我没有使用角形材料,但是它可以工作(我使用@ work)。这是角度6的目标,但适用于所有以前的版本。

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

谢谢

维杰


0

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

在要创建组件的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可以相同,并定义您的元数据。

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


-1

在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;

  }

}

希望这可以帮助。

干杯!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.