Angular 2.1.0 动态创建子组件 [英] Angular 2.1.0 create child component on the fly, dynamically

查看:35
本文介绍了Angular 2.1.0 动态创建子组件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 angular 2.1.0 中尝试做的是动态创建子组件,这些子组件应该注入到父组件中.例如,父组件是 lessonDetails,它包含所有课程的共享内容,例如 转到上一课转到下一课 等按钮和其他内容.根据路由参数,应该是子组件的课程内容需要动态注入父组件.子组件(课程内容)的 HTML 被定义为外面某处的纯字符串,它可以是像这样的对象:

What i'm trying to do in angular 2.1.0 is creating child components on the fly which should be injected into parent component. For example parent component is lessonDetails which contains shared stuff for all lessons such as buttons like Go to previous lesson, Go to next lesson and other stuff. Based on route params, lesson content which should be child component needs to be injected dynamically into parent component. HTML for child components (lesson content) is defined as plain string somewhere outside, it can be object like:

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

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

Problem can be easily solved through innerHtml having something like following in parent component template.

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

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

Where on each change of route params, property lessonContent of parent component would change(content(new template) would be taken from LESSON object) causing parent component template to be updated. This works but angular will not process content injected through innerHtml so it is impossible to use routerLink and other stuff.

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

Before new angular release i solved this problem using solution from http://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/, where i have been using ComponentMetadata together with ComponentResolver to create child components on the fly, like:

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

templateString 作为 Input 属性传递给子组件的地方.MetaDataComponentResolverangular 2.1.0 中均已弃用/删除.

Where templateString was passed to child component as Input property to child component. Both MetaData and ComponentResolver are deprecated/removed in angular 2.1.0.

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

So problem is not just about dynamic component creation, like described in few related SO questions, problem would be easier to solve if i would have defined component for each lesson-content. This would mean that i need to predeclare 100 different components for 100 different lessons. Deprecated Metadata was providing behaviour that was like updating template at runtime of single component(creating and destroying single component on route params change).

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

Update 1: As it seems in recent angular release, all components that needs to be created/injected dynamically needs to be predefined in entryComponents within @NgModule. So as it seems to me, related to question above, if i need to have 100 lessons(components that needs to be created dynamically on the fly) that means i need to predefine 100 components

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

Update 2: Based on Update 1, it can be done through ViewContainerRef.createComponent() in following way:

// 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 ]

现在在路由参数的父组件中更改

Now in parent component on route params change

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() 为:

Where lessonContentContainer is decorated @ViewChildren property and lessonContent is decorated as @ViewChild and it is initialized in ngAfterViewInit () as:

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)都需要预定义.
有没有办法使用单个组件并更改该组件的模板在运行时(在路由参数更改时)?

Solution has one drawback and that is, all components(LESSON_CONTENT_COMPONENTS) needs to be predefined.
Is there a way to use one single component and to change template of that component at runtime (on route params change)?

推荐答案

您可以使用以下 HtmlOutlet 指令:

You can use the following HtmlOutlet directive:

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

See also Plunker Example

自定义组件示例

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

For AOT compilation see these threads

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

这篇关于Angular 2.1.0 动态创建子组件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆