承接上文,本文将从一个基本的angular启动项目开始搭建一个具有基本功能、较通用、低耦合、可扩展的popup弹窗(脸红),主要分为以下几步:
- 基本项目结构搭建
- 弹窗服务
- 弹窗的引用对象
- 准备作为模板的弹窗组件
- 使用方法
基本项目结构
因为打算将我们的popup弹窗设计为在npm托管的包,以便其他项目可以下载并使用,所以我们的启动项目大概包含如下结构:
- package.json // 定义包的基本信息,包括名字、版本号、依赖等
- tsconfig.json // angular项目基于typescript进行搭建,需要此文件来指定ts的编译规则
- ... // tslint等一些帮助开发的配置文件
- index.ts // 放在根目录,导出需要导出的模块、服务等
- /src // 实际模块的实现
- /src/module.ts // 模块的定义
- /src/service.ts // 弹窗服务
- /src/templates/* // 作为模板的组件
- /src/popup.ref.ts // 对创建好的组件引用的封装对象
- /src/animations.ts // 动画的配置
现在我们只来关心src目录下的实现。
弹窗服务
弹窗服务的职责是提供一个叫做open的方法,用来创建出组件并显示,还得对创建好的组件进行良好的控制:
import { Injectable, ApplicationRef, ComponentFactoryResolver,
ComponentRef, EmbeddedViewRef } from '@angular/core';
import { YupRef, ComponentType } from './popup.ref';
@Injectable()
export class DialogService {
private loadRef: YupRef<LoadComponent>;
constructor(
private appRef: ApplicationRef,
private compFactRes: ComponentFactoryResolver
) {}
// 创建一个组件,组件通过泛型传入以做到通用
public open<T>(component: ComponentType<T>, config: any) {
// 创建组件工厂
const factory = this.compFactRes.resolveComponentFactory(component);
// 创建一个新的弹窗引用
const dialogRef = new YupRef(factory, config);
// 将创建好的组件引用(由弹窗引用创建并返回)append到body标签下
window.document.body.appendChild(this.getComponentRootNode(dialogRef.componentRef()));
// 加入angular脏检查
this.appRef.attachView(dialogRef.componentRef().hostView);
// 将创建的弹窗引用返回给外界
return dialogRef;
}
// 参考自Material2,将ComponentRef类型的组件引用转换为DOM节点
private getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {
return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
}
}
// 参考自Material2 用于作为传入组件的类型
export interface ComponentType<T> {
new (...args: any[]): T;
}
弹窗的引用对象
上面服务中的open方法实际上把创建组件的细节通过new一个YupRef即弹窗引用来实现,这是因为考虑到服务本身是单例,如果仅使用open方法直接创建多个弹窗,在使用时会丢失除了最后一个弹窗外的控制能力,笔者这里采用的办法是将创建的弹窗封装成一个类即YupRef:
import { ComponentRef, InjectionToken, ReflectiveInjector, ComponentFactory } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
// 用于注入自定义数据到创建的组件中
export const YUP_DATA = new InjectionToken<any>('YUPPopupData');
export class YupRef<T> {
// 弹窗关闭的订阅
private afterClose$: Subject<any>;
// 弹窗引用变量
private dialogRef: ComponentRef<T>;
constructor(
private factory: ComponentFactory<T>,
private config: any // 传入的自定义数据
) {
this.afterClose$ = new Subject<any>();
this.dialogRef = this.factory.create(
ReflectiveInjector.resolveAndCreate([
{provide: YUP_DATA, useva lue: config}, // 注入自定义数据
{provide: YupRef, useva lue: this} // 注入自身,这样就可以在创建的组件里控制组件的关闭等
])
);
}
// 提供给外界的对窗口关闭的订阅
public afterClose(): Observable<any> {
return this.afterClose$.asObservable();
}
// 关闭方法,将销毁组件
public close(data?: any) {
this.afterClose$.next(data);
this.afterClose$.complete();
this.dialogRef.destroy();
}
// 提供给弹窗服务以帮助添加到DOM中
public componentRef() {
return this.dialogRef;
}
}
这样一来每次调用open方法后都能得到一个YupRef对象,提供了关闭方法以及对关闭事件的订阅方法。
预制弹窗组件
弹窗服务中的open方法需要两个参数,第二个是传入的自定义数据,第一个就是需要创建的组件了,现在我们创建出几个预制组件,以dialog.component为例:
import { Component, Injector } from '@angular/core';
import { YupRef, YUP_DATA } from '../popup.ref';
import { mas