Angular2组件库 - Modal组件实现详解(一)

       这篇文章其实写的有点晚了。去年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新版本中已经被修复,但很多问题还是值得借鉴的。

上一篇:Angular CLI 帮助开发者快速创建Angular 2项目和组件


下一篇:运维前线:一线运维专家的运维方法、技巧与实践2.4 如何利用Python获取Facts