Angular 2.1.0 动态创建子组件

Posted

技术标签:

【中文标题】Angular 2.1.0 动态创建子组件【英文标题】:Angular 2.1.0 create child component on the fly, dynamically 【发布时间】:2017-02-24 21:49:08 【问题描述】:

我在angular 2.1.0 中尝试做的是动态创建子组件,这些子组件应该被注入到父组件中。例如父组件是lessonDetails,其中包含所有课程的共享内容,例如Go to previous lessonGo to next lesson 等按钮。根据路由参数,应该是子组件的课程内容需要动态注入到父组件中。子组件(课程内容)的 html 被定义为外部某处的纯字符串,它可以是如下对象:

export const LESSONS = 
  "lesson-1": `<p> lesson 1 </p>`,
  "lesson-2": `<p> lesson 2 </p>`

问题可以通过innerHtml父组件模板中具有类似以下的内容轻松解决。

<div [innerHTML]="lessonContent"></div>

在每次更改路由参数时,父组件的属性lessonContent 会发生变化(内容(新模板)将从LESSON 对象中获取)导致父组件模板被更新。这可行,但 Angular 不会处理通过 innerHtml 注入的内容,因此无法使用 routerLink 和其他东西。

在新的角度发布之前,我使用http://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/ 的解决方案解决了这个问题,我一直使用ComponentMetadataComponentResolver 来动态创建子组件,例如:

const metadata = new ComponentMetadata(
  template: this.templateString,
);

其中templateString 被传递给子组件作为Input 属性传递给子组件。 MetaDataComponentResolverangular 2.1.0 中已弃用/删除。

所以问题不仅仅是关于动态组件的创建,就像在一些相关的 SO 问题中描述的那样,如果我为每个课程内容定义了组件,问题将更容易解决。这意味着我需要为 100 个不同的课程预先声明 100 个不同的组件。已弃用的元数据提供的行为类似于在单个组件的运行时更新模板(在路由参数更改时创建和销毁单个组件)。

更新 1: 在最近的 Angular 版本中,所有需要动态创建/注入的组件都需要在 entryComponents 中的 @NgModule 中进行预定义。所以在我看来,与上述问题相关,如果我需要 100 个课程(需要动态创建的组件),这意味着我需要预定义 100 个组件

更新2:在更新1的基础上,可以通过ViewContainerRef.createComponent()通过以下方式完成:

// lessons.ts
@Component( template: html string loaded from somewhere )
class LESSON_1 

@Component( template: html string loaded from somewhere )
class LESSON_2 

// exported value to be used in entryComponents in @NgModule
export const LESSON_CONTENT_COMPONENTS = [ LESSON_1, LESSON_2 ]

现在在父组件中路由参数发生变化

const key = // determine lesson name from route params

/**
 * class is just buzzword for function
 * find Component by name (LESSON_1 for example)
 * here name is property of function (class)
 */

const dynamicComponent = _.find(LESSON_CONTENT_COMPONENTS,  name: key );
const lessonContentFactory = this.resolver.resolveComponentFactory(dynamicComponent);
this.componentRef = this.lessonContent.createComponent(lessonContentFactory);

父模板如下:

<div *ngIf="something" #lessonContentContainer></div>

其中lessonContentContainer 被修饰为@ViewChildren 属性,lessonContent 被修饰为@ViewChild,并在ngAfterViewInit () 中初始化为:

ngAfterViewInit () 
  this.lessonContentContainer.changes.subscribe((items) => 
    this.lessonContent = items.first;
    this.subscription = this.activatedRoute.params.subscribe((params) => 
      // logic that needs to show lessons
    )
  )

解决方案有一个缺点,即所有组件(LESSON_CONTENT_COMPONENTS)都需要预定义。有没有办法使用单个组件并更改该组件的模板在运行时(路由参数更改)?

【问题讨论】:

见hl7.org/fhir/StructureDefinition/…。添加 HTML 只是添加 HTML,如果您想要动态组件,您可以使用 ViewContainerRef.createComponent()。否则,只会为静态添加到组件模板的选择器创建组件和指令。 @GünterZöchbauer 感谢您的回复,实际上我正在使用 ViewContainerRef.createComponent() 请检查有问题的更新 2 部分 您不能在运行时修改组件的模板。有一些方法可以在运行时创建新组件。我不知道这方面的细节,但在 SO 上有类似问题的答案。 这里有类似的问题How can I use/create dynamic template to compile dynamic Component with Angular 2.0? 【参考方案1】:

您可以使用以下HtmlOutlet 指令:

import 
  Component,
  Directive,
  NgModule,
  Input,
  ViewContainerRef,
  Compiler,
  ComponentFactory,
  ModuleWithComponentFactories,
  ComponentRef,
  ReflectiveInjector
 from '@angular/core';

import  RouterModule   from '@angular/router';
import  CommonModule  from '@angular/common';

export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> 
    const cmpClass = class DynamicComponent ;
    const decoratedCmp = Component(metadata)(cmpClass);

    @NgModule( imports: [CommonModule, RouterModule], declarations: [decoratedCmp] )
    class DynamicHtmlModule  

    return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
       .then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => 
        return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
      );


@Directive( selector: 'html-outlet' )
export class HtmlOutlet 
  @Input() html: string;
  cmpRef: ComponentRef<any>;

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

  ngOnChanges() 
    const html = this.html;
    if (!html) return;

    if(this.cmpRef) 
      this.cmpRef.destroy();
    

    const compMetadata = new Component(
        selector: 'dynamic-html',
        template: this.html,
    );

    createComponentFactory(this.compiler, compMetadata)
      .then(factory => 
        const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);   
        this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
      );
  

  ngOnDestroy() 
    if(this.cmpRef) 
      this.cmpRef.destroy();
        
  

另请参阅Plunker Example

Example with custom component

对于 AOT 编译,请参阅这些线程

https://github.com/angular/angular/issues/15510 http://blog.assaf.co/angular-2-harmony-aot-compilation-with-lazy-jit-2/

另见 github Webpack AOT 示例 https://github.com/alexzuza/angular2-build-examples/tree/master/ngc-webpack

【讨论】:

感谢您的回答。还有一个问题,我们不应该保存对当前cmpRef 的引用并在创建新的动态组件之前手动销毁它吗?在创建新组件 if (this.cmpRef) this.cmpRef.destroy(); 之前,在 HtmlOutlet 指令中,private cmpRef: ComponentRef&lt;any&gt; 然后在 ngOnChanges 内部。还是会自动销毁? 是的,当然。我们必须手动完成。我不确定,但似乎this.vcRef.clear 做同样的事情。我更新了我的答案 再问一个问题,有点与问题无关。尝试在 html 出口指令上应用safeHtml 管道作为&lt;html-outlet [html]="htmlString | safeHtml&gt;&lt;/html-outlet&gt;" 得到错误Cannot set property stack of [object Object] which has only a getter(…)。 safeHtml 是一个非常简单的管道,将 transform 方法实现为 transform (html: string) return this.sanitizer.bypassSecurityTrustHtml(html); 相关回答***.com/questions/38037760/… 使用这个解决方案,但是由于新的 angular-cli 版本,它抛出了这个错误:没有为 'DynamicHtmlModule' 找到 NgModule 元数据。有什么建议吗?

以上是关于Angular 2.1.0 动态创建子组件的主要内容,如果未能解决你的问题,请参考以下文章

子动态创建的组件与父组件之间的Angular4通信

React - 使用 onClick 创建子组件的动态列表

如何动态命名和创建子对象属性?

34 动态组件(切换组件)保存状态input值

Angular完全销毁动态创建的组件

在 Angular 中动态创建的嵌套组件