这篇文章其实写的有点晚了。去年6月份,Angular2的版本刚升级到rc-4,一切都还处于蛮荒时期(虽然现在依然不是太稳定...)。当时为我们的组件库开发Modal组件,因为严格遵守ant design的规范来开发,所以Modal包含了Directive和Service两种模式
Directive模式非常符合Angular2的设计思想,所以开发过程也是顺风顺水。使用的方法也非常的常规。
import { Component } from '@angular/core';
@Component({
selector: 'nz-demo-modal-basic',
template: `
<button nz-button [nzType]="'primary'" (click)="showModal()">
<span>显示对话框</span>
</button>
<nz-modal [nzVisible]="isVisible" [nzTitle]="'第一个 Modal'" [nzContent]="modalContent" (nzOnCancel)="handleCancel($event)" (nzOnOk)="handleOk($event)">
<template #modalContent>
<p>对话框的内容</p>
<p>对话框的内容</p>
<p>对话框的内容</p>
</template>
</nz-modal>
`,
styles:[]
})
export class nzDemoModalBasic {
private isVisible: boolean = false;
private showModal = () => {
this.isVisible = true;
}
private handleOk = (e) => {
console.log('点击了确定');
this.isVisible = false;
}
private handleCancel = (e) => {
console.log(e);
this.isVisible = false;
}
constructor() {}
}
但实际上这中Directive的方法局限性很大,需要预设好Modal的内容,使用的时候需要对Modal进行显示和隐藏。这样做的弊端非常明显,需要Modal内部的内容相对固定,如果一个页面有很多使用Modal的地方,需要再template里加入很多的modal代码,非常难维护,使用起来也不灵活。
所以我们平时更多使用的是Service的模式,调用组件库提供的一个方法,例如ModalService.open(...)
传入一些配置,就可以根据配置创建并展示相应的Modal,关闭弹窗的时候,Modal同时被销毁。来无影去无踪,用起来轻松惬意。
但是理想很丰满,现实很骨感。在实现Service模式的过程中碰到了无数的坑,几乎翻遍了google上所有的angular2相关的*、博客和gitbub。总共花费了近一个月时间,完成了Modal主体功能开发。接着中间经历了Angular2版本从rc4 -> rc5 -> 稳定版的一波波框架大改,最后终于在11月份完成所有的改动。
Service的内容能够支持三种不同的格式:
- 文本
- 自定义模板
- 自定义Component
并且能够支持自定义Component的内外数据交互。最终实现的效果是这样的:
import { Component, TemplateRef, ContentChild, Input } from '@angular/core';
import { NzModalService } from '../../components/nz-modal';
import { nzDemoComponent } from './nz-modal-customize.component';
@Component({
selector: 'nz-demo-modal-service',
template: `
<button nz-button [nzType]="'primary'" (click)="showModal()">
<span>使用文本</span>
</button>
<button nz-button [nzType]="'primary'" (click)="showModalForTemplate(title, content, footer)">
<span>使用模板</span>
</button>
<template #title>
<span>对话框标题模板</span>
</template>
<template #content>
<div>
<p>对话框的内容</p>
<p>对话框的内容</p>
<p>对话框的内容</p>
<p>对话框的内容</p>
<p>对话框的内容</p>
</div>
</template>
<template #footer>
<div>
<button nz-button [nzType]="'primary'" [nzSize]="'large'" (click)="handleOk($event)" [nzLoading]="isConfirmLoading">
提 交
</button>
</div>
</template>
<button nz-button [nzType]="'primary'" (click)="showModalForComponent()">
<span>使用Component</span>
</button>
`
})
export class nzDemoModalService {
private currentModal;
private isConfirmLoading: boolean = false;
constructor(private modalService: NzModalService) { }
private showModal() {
let modal = this.modalService.open({
title: '对话框标题',
content: '纯文本内容,点确认 1 秒后关闭',
closable: false,
onOk() {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
},
onCancel() {}
});
}
private showModalForTemplate(titleTpl, contentTpl, footerTpl) {
this.currentModal = this.modalService.open({
title: titleTpl,
content: contentTpl,
footer: footerTpl,
maskClosable: false,
onOk() {
console.log('Click ok');
}
});
}
public showModalForComponent() {
this.modalService.open({
title: '对话框标题',
content: nzDemoComponent,
onOk() { },
onCancel() {
console.log('Click cancel');
},
footer: false,
componentParams: {
name: '测试渲染Component'
}
});
}
private handleOk(e) {
this.isConfirmLoading = true;
setTimeout(() => {
/* destroy方法可以传入onOk或者onCancel。默认是onCancel */
this.currentModal.destroy('onOk');
this.isConfirmLoading = false;
this.currentModal = null;
}, 1000);
}
}
/* 用户自定义的component nzDemoComponent的代码如下
import { Component, Input } from '@angular/core';
import { nzModalSubject } from '../../components/nz-modal';
@Component({
selector: 'nz-demo-component',
template: `
<div>
<h4>{{_name}}</h4>
<br />
<p>Modal打开3秒后自动关闭</p>
<div class="customize-footer">
<button nz-button [nzType]="'ghost'" [nzSize]="'large'" (click)="handleCancel($event)">
返 回
</button>
</div>
</div>
`,
styles: [
`
:host >>> .customize-footer {
border-top: 1px solid #e9e9e9;
padding: 10px 18px 0 10px;
text-align: right;
border-radius: 0 0 0px 0px;
margin: 15px -16px -5px -16px;
}
`
]
})
export class nzDemoComponent {
private _name: string;
@Input()
public set name(value: string) {
this._name = value;
}
handleCancel() {
this.subject.destroy('onCancel');
}
constructor(private subject: nzModalSubject) {
this.subject.on('onDestory', () => {
console.log('destroy');
});
}
ngOnInit() {
setTimeout(() => {
this.subject.destroy();
}, 3000);
}
}
*/
在开发过程中碰到无数的问题,在这第一篇中我会分享一下最大的一个问题。其他的问题我会在第二篇中来细讲。
那么这个最大的问题是什么呢?这个问题就是通过原生js在body里加入一个<nz-modal></nz-modal>
的dom,如果将这个dom加入到Angular的zone里进行modal的初始化。这个问题的关键是Angular2的API:ApplicationRef.bootstrap
主要的代码如下:
private _open(props: configInterface, factory: ComponentFactory<any>): nzModalSubject {
// 在body的内部最前插入一个<nz-modal></nz-modal>方便进行ApplicationRef.bootstrap
document.body.insertBefore(document.createElement(factory.selector), document.body.firstChild);
let customComponentFactory: ComponentFactory<any>;
let compRef: ComponentRef<any>;
// 判断如果nzContent是用户自定义的component对象,则将该对象转成ComponentFactory,再通过props传入modal来作为内容渲染
if (props['nzContent'] instanceof Type) {
customComponentFactory = this._cfr.resolveComponentFactory(props['nzContent']);
// 将编译出来的ngmodule中的用户component的factory作为modal内容存入
props['nzContent'] = customComponentFactory;
}
// *关键所在,this._appRef是当前app的ApplicationRef
compRef = this._appRef.bootstrap(factory);
// 通过compRef.instance可以拿到Modal的对象
instance = compRef.instance;
// subject用于Modal的内外component的交互
subject = instance.subject;
...
// 可以直接通过对象的合并来将用户定义的参数传入Modal对象
Object.assign(instance, props, {
nzVisible: true
});
return subject;
}
甚至在开发Modal的那段时间,Bootstrap也出了Angular2的组件库。我原本希望借鉴一下Bootstrap是如何实现Modal的Service实现,但是非常可惜,当时Bootstrap也没有提供Service模式的Modal,甚至但是业内都没有。直到12月份的NG大会上,Bootsrap团队才带来Service模式的实现,视频在这里https://www.youtube.com/watch?v=EMjTp12VbQ8。
Bootstrap团队的实现方式跟我的方法是一样的。我当时能发现这个方法,也是运气。当时已经几乎用尽所有的办法,只能尝试去看Angular2的源代码,最后在源代码里找到了灵感,而且当时文档也不完善,这个方法并没有被挖掘。不过我是不是世界上第一个找到这个方法来实现的,也不好说,毕竟前端界大牛太多,很多人可能找到了但没有分享出来而已。
Modal的第一篇就到这里,第二篇中我会详细分享开发Modal中所有踩过的坑,有些可能在现在的Angular2新版本中已经被修复,但很多问题还是值得借鉴的。