Angular 2.1.0 动态创建子组件

IT技术 javascript angular
2021-01-15 02:07:55

我想要做的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和其他东西。

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

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

WheretemplateString作为Input属性传递给子组件。双方MetaDataComponentResolver已被弃用/去掉angular 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)都需要预定义。
有没有办法使用单个组件并在运行时更改该组件的模板(在路由参数更改时)?

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示例

自定义组件示例

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

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

当然是。我们必须手动完成。我不确定,但似乎this.vcRef.clear做同样的事情。我更新了我的答案
2021-03-17 02:07:55
还有一个问题,与问题有点无关。尝试safeHtml在 html outlet 指令上应用管道作为<html-outlet [html]="htmlString | safeHtml></html-outlet>"获取错误Cannot set property stack of [object Object] which has only a getter(…)safeHtml 是一个非常简单的管道,其transform方法实现为transform (html: string) { return this.sanitizer.bypassSecurityTrustHtml(html); }
2021-03-19 02:07:55
谢谢你的好答案。还有一个问题,我们不应该cmpRef在创建新的动态组件之前保存对 current 的引用并手动销毁它吗?在 HtmlOutlet 指令中有类似的东西,private cmpRef: ComponentRef<any>然后ngOnChanges在创建新组件之前在里面if (this.cmpRef) { this.cmpRef.destroy(); }还是会自动销毁?
2021-04-05 02:07:55
使用此解决方案,但由于新的 angular-cli 版本,它抛出此错误:未找到“DynamicHtmlModule”的 NgModule 元数据。有什么建议吗?
2021-04-08 02:07:55
2021-04-12 02:07:55