Angular2 动态创建组件

在ng1.x时我写过Angular 动态创建 directive;而在Angular2应用是由一个庞大的组件树来构建业务,一般而言我们是通过 template 模板来动态加载组件。

一、原因

我依然以1.x所遇到的项目实现为原型。

我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

二、创建组件

首先需要先创建了两个组件 richconfig

src/components/rich.ts

import { Component } from '@angular/core';

@Component({
  selector: 'rich',
  template: `
    <p>Rich Content</p>
  `
})

export class RichComponent { }

src/components/config.ts

import { Component } from '@angular/core';

@Component({
  selector: 'config',
  template: `
    <p>Config Content</p>
  `
})

export class ConfigComponent { }

三、利用模板动态加载组件

template 模板里面加载组件是最常用加载方式,而且非常简单,只需要存放相应的 selector 模板引擎会自己查找并编译。

//our root app component
import {Component} from '@angular/core'

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

@Component({
  selector: 'template-load',
  providers: [],
  template: `
      <h1>方式一:通过 template 动态加载</h1>
      <config></config>
      <rich></rich>
  `,
  directives: [ ConfigComponent, RichComponent ]
})
export class TemplateLoadComponent {
  constructor() {

  }
}

这样的方式很明显的一点是,template的结构将决定组件在组件树当中所处的位置。假如我们要把 configrich 组件对换位置,不得不对 template 重新编写,这样更别说让用户来拖拽来调整组件显示位置这么高级的办法了。

四、利用 DynamicComponentLoader 实现动态加载

DynamicComponentLoader 支持两种不同的加载方式来动态创建组件;但在这之前我们需要先做点什么。

首先,我示例里是根据按钮点击事件创建相应的组件,所以我们需要先创建一个组件映射表。

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

export class DynamicLoadComponent {
  compMaps: {[key: string]: any} = {},
  constructor() {
    this.compMaps.config = ConfigComponent;
    this.compMaps.rich = RichComponent;
  }
}

这样我们可以模板里面定义添加事件,并传递相应的key值,下面是分别定义两个按钮用来创建 configrich 组件,以及 container1 元素用于放置组件位置。

<button type="button" (click)="add('config')">Add Config</button>
<button type="button" (click)="add('rich')">Add Rich</button>
<div id="container1"></div>

准备工作完成后,我们分别来看一下二种方式的差异:

1、loadAsRoot

loadAsRoot(type: Type, overrideSelectorOrNode: string|any, injector: Injector, onDispose?: () => void, projectableNodes?: any[][]) : Promise<ComponentRef<any>>

loadAsRoot 方法会创建一个 type 组件实例,然后放到 overrideSelectorOrNode 放处位置的第一个元素下面,不管你动态加载几次永远都只有一份组件实例

constructor(public dcl:DynamicComponentLoader, public _injector:Injector) {
}

add(comp) {
  this.dcl.loadAsRoot(this.compMaps[comp], "#container1", this._injector);
}

#container1 位置可以在整个文档的任意位置,只要该ID是存在。

最终的DOM结构像这样:

<div id="container1">
   <p>Config Content</p>
</div>

loadAsRoot 方法很好理解,而且很霸道,始终只会创建一个组件实例。

2、loadNextToLocation

loadNextToLocation(type: Type, location: ViewContainerRef, providers?: ResolvedReflectiveProvider[], projectableNodes?: any[][]) : Promise<ComponentRef<any>>

同样也是创建一个 type 组件实例,并插入到指定 location 位置的后面,ViewContainerRef 是一个视图容器,所以通过 loadNextToLocation 方法加载的组件,都会放入容器里面。

首先需要在模板中定义一个本地模板变量 container2,并在组件类中获取该变量。

@Component({
   template: `
    <div #container2></div>
   `
})

export class DynamicLoadComponent {
  @ViewChild('container2', {read: ViewContainerRef}) target;
}

add 方法也非常简单:

  add(type) {
    this.dcl.loadNextToLocation(this.compMaps[comp], this.target)
  }

最终的DOM结构像这样:

<config>
  <p>Config Content</p>
</config>
<rich>
  <p>Rich Content</p>
</rich>

3、传递数据

两种方法返回的结果都是一个 Promise 类型,并传递一个 ComponentRef 的新创建组件对象。


this.dcl.loadNextToLocation(this.compMaps[comp], this.target).then(function(cmpRef) { // doing })

可以通过 cmpRef 参数进行数据传递,比如:

cmpRef.instance.someProperty = someData;

五、总结

文章中只是做个抛砖引玉,实际项目应该做更深一层的抽象,Angular2是以组件为出发点,不能有太多了依赖。但是,文章至少已经解决最核心的问题了。文章并没有给出完整代码,如您感兴趣见Plunker

DynamicComponentLoader 从变更纪录来看非常频繁,在前两个BETA版本中都对其做大量的变动;但不管怎么变,是越来越精简。

1 Comment

  1. 学习了,对我来说有帮助,因为目前正好在找这个相关资料

发表评论

Your email address will not be published.

*

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top