Angular2 动态的创建组件并插入到Shadow Dom中
作者随时修改,为方便读者追本朔源,转载请保留地址。
前言:
为什么会有这个需求?
因为在开发组件中,难免会有一些组件是需要动态生成的,以减少Document中Dom 数量,节省内存开支。
例如全局的 message 组件、Alert 组件、Notice 组件等。
angular2 中如何动态的编译Template?
在 Ng2 中, 废除了 $compiled 这个方法,用户将不能直接编译模板,如果想动态的创建组件,必须借助 组件工厂 (componentFactoryResolver)
如何动态创建一个组件:
这里以 Tooltip 组件为例
- 首先需要准备一个组件内容(用来插入到页面中的动态组件)
这个组件就是很常规的组件, 没有什么特别的东西,只需要准备一些变量同步Template即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { Component } from '@angular/core';
@Component({ selector: 'pxx-tooltip-container', templateUrl: './tooltip.html', styleUrls: ['./tooltip.scss'], }) export class TooltipContainer { _top: any; _left: any; _state = ''; message: string; placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
_show: boolean = false; }
|
- 将这个内容组件添加到Module 中,这一步主要是将这个组件存入 Angular 的工厂缓存中。
App.module (或者是其他的Module,甚至是这个组件自身的Module 都可以,只要最终在AppModule 中 import), 我这里是组件自身的Module,最终导入到App.Module中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| NgModule({ imports: [ TooltipModule ], declarations: [ TooltipContainer ], exports: [ TooltipContainer ], entryComponents: [ TooltipContainer ] }) export class SharedModule { static share(): ModuleWithProviders { return { ngModule: SharedModule, providers: [] }; }
}
|
- 建立一个中转组件、或者 service 来操作生成的组件, 这里我创建了一个 Tooltip.component.ts 的组件来做中转,也是用户真实使用的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| import { Component, Input, HostListener, ComponentRef, ComponentFactoryResolver, ApplicationRef, ViewContainerRef, ReflectiveInjector } from '@angular/core'; import { TooltipContainer } from './tooltip.container';
@Component({ selector: 'pxx-tooltip', template: '<ng-content></ng-content>', }) export class TooltipComponent { container: ComponentRef<any>;
@Input() message: string; @Input() placement: 'top' | 'bottom' = 'top';
private _top: any; private _left: any;
constructor(private componentFactoryResolver: ComponentFactoryResolver, private appRef: ApplicationRef) { }
@HostListener('mouseenter', ['$event.target']) enter(el) { this._createTips().then( () => { this._getOffset(el); }); this._createTimeout('enter', 200); this.container.instance._show = true; }
@HostListener('mouseleave', ['$event.target']) leave() { this._createTimeout('leave', 80, () => { this.container.destroy(); this.container = null; }); }
private _createTips<T>(): Promise<T> { return new Promise((resolve, reject) => { if (!this.container) { if (!this.appRef['_rootComponents'].length) { const err = new Error('AppRoot 未找到.'); console.error(err); reject(err); }
let appContainer: ViewContainerRef = this.appRef['_rootComponents'][0]['_hostElement'].vcRef;
let providers = ReflectiveInjector.resolve([ ]);
let tooltipFactory = this.componentFactoryResolver.resolveComponentFactory(TooltipContainer); let childInjector = ReflectiveInjector.fromResolvedProviders(providers, appContainer.parentInjector); this.container = appContainer.createComponent(tooltipFactory, appContainer.length, childInjector);
this.container.instance.placement = this.placement; this.container.instance.message = this.message;
setTimeout(() => resolve(), 0); }
}); }
private _getOffset(el) { let tooltip = <HTMLElement>document.querySelector('#tooltip');
if (this.placement == 'top') {
this._left = (el.getBoundingClientRect().left + (el.offsetWidth - tooltip.offsetWidth) / 2) + 'px'; this._top = el.getBoundingClientRect().top - el.offsetHeight - tooltip.offsetHeight + (el.offsetHeight / 1.28) + 'px';
} else if (this.placement == 'bottom') { this._left = (el.getBoundingClientRect().left + (el.offsetWidth - tooltip.offsetWidth) / 2) + 'px'; this._top = el.getBoundingClientRect().top + el.offsetHeight + 'px'; }
this.container.instance._left = this._left; this.container.instance._top = this._top; }
private _createTimeout(state, delay, cb?: Function) { this.container.instance._state = state; setTimeout(() => { cb && cb(); }, delay); } }
|
- 将 TooltipComponent 这个中转组件也添加进APPModule
- 使用:
1
| <pxx-tooltip message="这是一个优雅的提示">Hover </pxx-tooltip>
|
** 关于方法二 **
这篇文章被我命名为 方法一, 所以还会有方法二。
相对于上文这种方法, 方法二功能更强大,然而更死板。
方法二的使用的核心API是 compileModuleAndAllComponentsAsync
RuntimeCompiler
.
方法二相对于方法一:
- 好处就是不用每次把需要动态加载的组件放到 entryComponent 中缓存。但是需要动态的创建 ComponentFactory,完全的构造一个新的Module,这个Module 自然就包含了动态创建的组件。
- 坏处就是创建的Module 完全独立,无论作用域、Module通信等。也就意味着创建出来的Module无法使用外部Module 已经 import 过的模块、指令、服务、组件等,这真让人感到悲伤。。
那么方法二有什么用呢?
个人觉得应用场景是用与解决动态的切换组件.
附
编辑于 2017-11
经过 Angular2 -> Angular4 -> Angular5 的频繁变更,上述所用 this.appRef['_rootComponents'][0]['_hostElement'].vcRef
这些非官方 public API 已经失效。
现方法为:this.appRef.components[0].instance.viewContainerRef;
.
目测为 stable ,毕竟没有下划线了。
另外:
在官方文档中发现了一个新的方法: ReflectiveInjector.resolveAndCreate
, 所以或许有更好的解法。本文权作参考即可。
参见 https://angular.io/api/core/ReflectiveInjector