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

时间:2016-08-11 05:37:07

标签: angular typescript compilation angular2-compiler

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

直到RC4我使用ComponentResolver,但是使用RC5我得到消息:

  不推荐使用

ComponentResolver进行动态编译。请将ComponentFactoryResolver@NgModule/@Component.entryComponents或ANALYZE_FOR_ENTRY_COMPONENTS提供程序一起使用。 仅限运行时编译,您也可以使用Compiler.compileComponentSync/Async

我找到了这个(官方的angular2)文件

Angular 2 Synchronous Dynamic Component Creation

了解我可以使用

  • 带有ngIf的动态ComponentFactoryResolver。如果我将已知组件传递到托管@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

的解决方案

AOT和JitCompiler (以前的RuntimeCompiler)

您是否希望将此功能与AOT(提前编译)一起使用?你得到了吗?

  

错误:静态解析符号值时出错。不支持函数调用。考虑使用对导出函数的引用替换函数或lambda(原始.ts文件中的位置65:17),在... / node_modules/@angular/compiler/src/compiler.d.ts中解析符号COMPILER_PROVIDERS,< / p>

请留下你的评论,在这里投票:

Could/would/will code using COMPILER_PROVIDERS be supported by AOT?

15 个答案:

答案 0 :(得分:151)

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

相关
  

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

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

在果壳中

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

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

以下是代码段(更多内容here - 我们的自定义构建器返回刚刚构建/缓存的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 ++&amp; 运行时组件

下面对this scenario的描述,我们将

  1. 创建一个模块PartsModule:NgModule (小块的持有者)
  2. 创建另一个模块DynamicModule:NgModule,其中包含我们的动态组件(并动态引用PartsModule
  3. 创建动态模板(简单方法)
  4. 创建新的Component类型(仅当模板已更改时)
  5. 创建新的RuntimeModule:NgModule。此模块将包含先前创建的Component类型
  6. 致电JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)以获取ComponentFactory
  7. 创建DynamicComponent的实例 - 查看目标占位符的作业和ComponentFactory
  8. @Inputs分配给新实例 (从INPUT切换到TEXTAREA编辑),消费@Outputs < / LI>

    NgModule

    我们需要一个NgModule

      

    虽然我想展示一个非常简单的例子,但在这种情况下,我需要三个模块(实际上是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中使用这个简单/天真的构建器。

      

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

    // 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构建器

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

      

    我们的组件类型(使用我们的DynamicTypeBuilder构建)可能会有所不同 - 但仅限于其模板(在上面创建)。组件&#39;属性(输入,输出或某些受保护)仍然相同。 如果我们需要不同的属性,我们应该定义Template和Type 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);
                });
        });
    }
    
      

    上面我们创建了缓存 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 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。所以它不必对变化做出反应。但是尽管事实(并准备好进行更改) - 如果组件已经(首先)启动,我们需要引入一些标志。只有这样,我们才能开启魔力。

    最后,我们将使用我们的组件构建器及其刚编译/缓存 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

    全面检查here

      

    查看此帖子的先前版本(例如RC5相关),查看history

答案 1 :(得分:51)

EDIT(26/08/2017):以下解决方案适用于Angular2和4.我已将其更新为包含模板变量并单击处理程序并使用Angular 4.3进行测试。
对于Angular4,Ophir's answer中描述的ngComponentOutlet是一个更好的解决方案。但是现在它does not support inputs & outputs了。如果[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

答案 2 :(得分:46)

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

我最终做的是使用Angular 4.0.0-beta.6的{​​{3}}。

这给了我最简单,最简单的解决方案,全部写在动态组件的文件中。

  • 这是一个简单的示例,它只接收文本并将其放在模板中,但显然您可以根据需要进行更改:
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 :(得分:15)

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

答案 4 :(得分:8)

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

说,你有一个动态的html模板= 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

答案 5 :(得分:7)

2019年2月答案

好消息!看来@angular/cdk软件包现在对portals具有一流的支持!

1)在您的模块中:

@NgModule({
  imports:      [ ..., PortalModule, ... ],
  entryComponents: [ChildComponent]
})
export class AppModule { }

2)在您的组件中:

import { Component, OnInit } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';

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


@Component({
  selector: 'my-child',
  template: `<p>I am a dynamic component!</p>`
})
export class ChildComponent  {
}

Stackblitz

答案 6 :(得分:5)

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

用法:

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

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

答案 7 :(得分:4)

我想在Radim的这篇非常优秀的帖子之上添加一些细节。

我采用了这个解决方案并对其进行了一些处理并很快遇到了一些限制。我将概述这些,然后给出解决方案。

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

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

recursive dynamic template compilation in angular2

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

要在彼此内部嵌套动态详细信息,您需要在 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您需要在DynamicDetail

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

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

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

可以在这里找到一个有效的修改过的plunker: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);
            });
    });

希望这有助于其他人!

祝你好运 的Morten

答案 8 :(得分:4)

关注Radmin的优秀答案,对于使用angular-cli版本1.0.0-beta.22及以上版本的每个人都需要进行一些调整。

COMPILER_PROVIDERS 无法再导入(有关详细信息,请参阅angular-cli GitHub)。

因此,解决方法是根本不在COMPILER_PROVIDERS部分使用JitCompilerproviders,而是使用来自&#39; @ angular / compiler&#的JitCompilerFactory 39;而是在类型构建器类中这样:

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

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

答案 9 :(得分:4)

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

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

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

答案 10 :(得分:2)

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

但是,我可以建议在这种情况下更清晰地创建组件 - 只需在模板中使用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>
顺便说一下,&#34; [&#34; in [prop]表达式有一个含义,这表示单向数据绑定,因此如果你知道你不需要将属性绑定到变量,你甚至可以省略它们。

答案 11 :(得分:1)

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

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

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

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

谢谢

维杰

答案 12 :(得分:1)

如果您需要作为解析动态字符串并通过其选择器加载组件的方式,您可能还会发现ngx-dynamic-hooks库很有用。我最初将其创建为个人项目的一部分,但是没有看到类似的东西,因此我对其进行了完善和公开。

一些花絮:

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

值得注意的是,它像其他响应一样,不是依赖运行时编译器。因此,您不能使用模板语法。另一方面,这意味着它既可以在JiT和AoT模式下工作,也可以在Ivy和旧模板引擎下工作,并且通常使用起来更加安全。

查看实际情况in this Stackblitz

答案 13 :(得分: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的配置可以相同,并且可以定义您的元数据。

取决于您的配置或输入类型,该指令应相应地起作用,并从受支持的类型开始,它将呈现适当的组件。如果没有,它将记录错误。

答案 14 :(得分:-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;

  }

}

希望这有帮助。

干杯!