219

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

直到我使用 RC4 ComponentResolver,但使用 RC5 我收到以下消息:

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

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

并了解我可以使用

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

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

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

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

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

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

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

模板正在更改,因此我无法使用ComponentFactoryResolver和传递现有模板...我需要一个带有Compiler.

4

16 回答 16

170

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

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

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

简而言之

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

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

这是一个代码片段(更多内容在这里 - 我们的自定义构建器返回刚刚构建/缓存ComponentFactory的视图目标占位符用于创建DynamicComponent

  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });

就是这样——简而言之。要获取更多详细信息..请阅读以下内容

.

TL&DR

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

.

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

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

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

模块

我们需要一个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中,我们使用这个简单/天真的构建器。

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

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

因此,我们正在触及解决方案的核心。Builder 将 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来创建一个组件。简而言之,这里是该组件的所有部分(如果需要,请在此处打开 plunker

我们先总结一下import语句:

import {Component, ComponentRef,ViewChild,ViewContainerRef}   from '@angular/core';
import {AfterViewInit,OnInit,OnDestroy,OnChanges,SimpleChange} from '@angular/core';

import { IHaveDynamicData, DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder }               from './template.builder';

@Component({
  selector: 'dynamic-detail',
  template: `
<div>
  check/uncheck to use INPUT vs TEXTAREA:
  <input type="checkbox" #val (click)="refreshContent(val.checked)" /><hr />
  <div #dynamicContentPlaceHolder></div>  <hr />
  entity: <pre>{{entity | json}}</pre>
</div>
`,
})
export class DynamicDetail implements AfterViewInit, OnChanges, OnDestroy, OnInit
{ 
    // wee need Dynamic component builder
    constructor(
        protected typeBuilder: DynamicTypeBuilder,
        protected templateBuilder: DynamicTemplateBuilder
    ) {}
    ...

我们只接收模板和组件构建器。接下来是我们的示例所需的属性(更多在评论中)

// reference for a <div> with #dynamicContentPlaceHolder
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef}) 
protected dynamicComponentTarget: ViewContainerRef;
// this will be reference to dynamic content - to be able to destroy it
protected componentRef: ComponentRef<IHaveDynamicData>;

// until ngAfterViewInit, we cannot start (firstly) to process dynamic stuff
protected wasViewInitialized = false;

// example entity ... to be recieved from other app parts
// this is kind of candiate for @Input
protected entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
  };

在这个简单的场景中,我们的托管组件没有任何@Input. 所以它不必对变化做出反应。但是尽管有这个事实(并为即将发生的变化做好准备) - 如果组件已经(首先)启动,我们需要引入一些标志。只有这样我们才能开始魔术。

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

protected refreshContent(useTextarea: boolean = false){
  
  if (this.componentRef) {
      this.componentRef.destroy();
  }
  
  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });
}

小扩展

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

// this is the best moment where to start to process dynamic stuff
public ngAfterViewInit(): void
{
    this.wasViewInitialized = true;
    this.refreshContent();
}
// wasViewInitialized is an IMPORTANT switch 
// when this component would have its own changing @Input()
// - then we have to wait till view is intialized - first OnChange is too soon
public ngOnChanges(changes: {[key: string]: SimpleChange}): void
{
    if (this.wasViewInitialized) {
        return;
    }
    this.refreshContent();
}

public ngOnDestroy(){
  if (this.componentRef) {
      this.componentRef.destroy();
      this.componentRef = null;
  }
}

完毕

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

在这里检查一切

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

于 2016-08-11T05:37:07.113 回答
59

编辑(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

于 2016-09-15T09:41:20.627 回答
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 库升级到 ^Angular 4.0.0

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

更新

也适用于角度 5。

于 2017-02-06T10:18:55.590 回答
23

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

看到它在行动

于 2019-01-24T14:04:49.473 回答
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 {}`
于 2016-09-29T14:39:16.603 回答
10

我有一个简单的例子来展示如何做 angular 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

于 2016-09-06T06:06:45.740 回答
6

在 Angular 7.x 中,我为此使用了 angular-elements。

  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
...

在这种情况下,我使用了自定义标签名称“用户图标”。

  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 传递对象,然后再次解析它。我找不到更好的解决方案。

于 2019-01-29T08:23:36.590 回答
5

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

用法:

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

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

于 2017-04-26T18:32:17.067 回答
5

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

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

因此,解决方法是根本不使用COMPILER_PROVIDERSandJitCompiler在该providers部分中,而是JitCompilerFactory在类型构建器类中使用 from '@angular/compiler' 代替:

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

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

于 2017-01-04T09:26:12.110 回答
4

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

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

  • 首先,我无法在动态细节中呈现动态细节(基本上将动态 UI 相互嵌套)。
  • 下一个问题是我想在解决方案中可用的部分中呈现动态细节。最初的解决方案也无法做到这一点。
  • 最后,无法在字符串编辑器等动态部分使用模板 URL。

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

angular2中的递归动态模板编译

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

要启用相互嵌套的动态细节,您需要在type.builder.ts的导入语句中添加 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],
})

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

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

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

希望这对其他人有帮助!

最好的问候莫腾

于 2016-10-05T11:26:16.413 回答
3

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

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

这个例子是动态表单控件在添加组件中(这是您可以从服务器获取表单控件的地方)。如果您看到 addcomponent 方法,您可以看到表单控件。在这个例子中,我没有使用角材料,但它可以工作(我正在使用@work)。这是 Angular 6 的目标,但适用于所有以前的版本。

AngularVersion 5 及以上版本需要添加 JITComplierFactory。

谢谢

维杰

于 2018-10-05T12:24:01.160 回答
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] 表达式中的“[” 有一个含义,这表示一种数据绑定方式,因此如果您知道不需要将属性绑定到变量,您可以甚至应该省略这些。

于 2016-08-11T07:31:45.350 回答
2

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

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

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

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

于 2021-08-17T15:42:14.573 回答
1

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

一些花絮:

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

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

在此 Stackblitz 中查看它的实际效果。

于 2020-08-26T06:54:35.707 回答
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可以相同并定义您的元数据。

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

于 2019-05-20T19:23:00.023 回答
0

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

  }

}

希望这可以帮助。

干杯!

于 2018-02-16T12:41:21.707 回答