承接上文,本文将从一个基本的angular启动项目开始搭建一个具有基本功能、较通用、低耦合、可扩展的popup弹窗(脸红),主要分为以下几步:

  1. 基本项目结构搭建
  2. 弹窗服务
  3. 弹窗的引用对象
  4. 准备作为模板的弹窗组件
  5. 使用方法

基本项目结构

因为打算将我们的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, useValue: config}, // 注入自定义数据
{provide: YupRef, useValue: 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 { mask, dialog } from '../animations'; @Component({
template: `
<div class="yup-mask" [@mask]="disp" (click)="!data?.mask && close(false)"></div>
<div class="yup-body" [@dialog]="disp">
<div class="yup-body-head">{{data?.title || '消息'}}</div>
<div class="yup-body-content">{{data?.msg || ' '}}</div>
<div class="yup-body-btns">
<div class="btn default" (click)="close(false)">{{data?.no || '取消'}}</div>
<div class="btn primary" (click)="close(true)">{{data?.ok || '确认'}}</div>
</div>
</div>
`,
styles: [`这里省略一堆样式`]
animations: [mask, dialog]
})
export class DialogComponent {
public data: {
title?: string,
msg?: string,
ok?: string,
no?: string,
mask?: string
};
public dialogRef: YupRef<DialogComponent>;
public disp: string;
constructor(
private injector: Injector
) {
this.data = this.injector.get(YUP_DATA);
this.dialogRef = this.injector.get(YupRef);
this.disp = 'init';
setTimeout(() => {
this.disp = 'on';
});
}
public close(comfirm: boolean) {
this.disp = 'off';
setTimeout(() => {
this.disp = 'init';
this.dialogRef.close(comfirm);
}, 300);
}
}

用笔者这种方式创建的组件有两个尴尬的小问题:

  • 不能使用隐式的依赖注入了,必须注入Injector服务来手动get到注入的两个依赖,即代码中的

    this.injector.get(YUP_DATA) 和 this.injector.get(YupRef) 。
  • 直接使用angular动画会失效,因为是暴力添加到DOM中的方式,必须手动setTimeout过等动画结束再真正销毁组件。

创建好组件后再服务中添加快捷创建此组件的方法:

public dialog(config: {
title?: string,
msg?: string,
ok?: string,
no?: string,
mask?: boolean
}) {
return this.open(DialogComponent, config);
}

额外需要提一点是虽然这样创建的组件没有被一开始就添加到页面中,仍然需要在所属模块的declaration中声明,并且还得在entryComponent中声明过,否则angular就会通过报错的方式让你这么做,就像下面这个弹窗模块的定义这样:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DialogComponent, AlertComponent, ToastComponent, LoadComponent } from './templates';
import { DialogService } from './service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({
declarations: [DialogComponent, AlertComponent, ToastComponent, LoadComponent],
imports: [ NoopAnimationsModule, CommonModule ],
exports: [],
providers: [DialogService],
entryComponents: [DialogComponent, AlertComponent, ToastComponent, LoadComponent]
})
export class YupModule {}

而此弹窗模块真正需要导出的东西有4个,都列在index.ts中:

export { YupModule } from './module'; // 需要在AppModule中引入
export { DialogService as Yup } from './service'; // 用于发起弹窗
export { YupRef, YUP_DATA } from './popup.ref'; // 用于创建自定义弹窗时提供控制

使用方法

最终在外界的使用方式如下:

constructor(
public yup: Yup // 其实是DialogService,被笔者改了名
) { } public ngOnInit() {
this.yup.dialog({msg: '弹不弹?', title: '我弹', ok: '弹弹', no: '别弹了', mask: true}).afterClose().subscribe((res) => {
if (res) {
console.log('点击了确定');
} else {
console.log('点击了取消');
}
});
}

当不想使用预制的弹窗组件时,大可以自行创建好一个组件,然后使用open方法:

this.yup.open(CustomComponent, '我是自定义数据').afterClose().subscribe((res) => {
console.log(`我已经被关闭了,不过我能携带出来数据: 【${res}】`);
});

乍一看是不是有点接近Material2的Dialog的使用呢

动态创建angular组件实现popup弹窗的更多相关文章

  1. vue通过extend动态创建全局组件(插件)学习小记

    测试环境:nodejs+webpack,例子是看文章的,注释为自己的理解 创建一个toast.vue文件: <template> <div class="wrap" ...

  2. pyqt动态创建一系列组件并绑定信号和槽(网友提供学习)

    # -*- coding: utf-8 -*- # python:2.x __author__ = 'Administrator' #如上图要求:创建指定多个复选框,一种是通过QT设计器Designe ...

  3. delphi怎么一次性动态删除(释放)数个动态创建的组件?

    比如procedure TForm1.Button1Click(Sender: TObject);vari:Integer;lbl: TLabel;beginfor i:=1 to 3 dobegin ...

  4. Angular动态创建组件之Portals

    这篇文章主要介绍使用Angular api 和 CDK Portals两种方式实现动态创建组件,另外还会讲一些跟它相关的知识点,如:Angular多级依赖注入.ViewContainerRef,Por ...

  5. Delphi 动态创建组件,单个创建、单个销毁

    效果图如下: 实现部分代码如下: var rec: Integer = 0; //记录增行按钮点击次数 implementation {$R *.dfm} //动态释放单个组件内存,即销毁组件 pro ...

  6. OAF 动态创建组件以及动态绑定属性

    在开发中,我们遇到以下一个需求. 一个表格左侧有5列是固定存在的,右侧有N列是动态生成的,并且该N列中第一列可输入,第二列是不可编辑的,但是是数字,如果小于0,那么就要显示为红色,重点标识出来. 首先 ...

  7. ViewContainerRef 动态创建视图

    Angular DOM 操作 相关的APIs和类: 查询DOM节点 template variable ref: 模版变量引用,相当于react中的ref ViewChild: 查询DOM,返回单个元 ...

  8. Lightning框架示例 - 动态建立Lightning组件

    动态建立Lightning组件 组件化前端开发是Lightning框架的优点之一.在进行Lightning应用开发时,我们可以将组件进行嵌套.引用,从而实现模块的封装和重用,提高开发效率. 组件的嵌套 ...

  9. Angular中懒加载一个模块并动态创建显示该模块下声明的组件

    angular中支持可以通过路由来懒加载某些页面模块已达到减少首屏尺寸, 提高首屏加载速度的目的. 但是这种通过路由的方式有时候是无法满足需求的. 比如, 点击一个按钮后显示一行工具栏, 这个工具栏组 ...

随机推荐

  1. DynamicXml

    /* var xml = @"<root><books><book is_read=""false""><a ...

  2. linux忘记密码/修改密码

    方法一:用set password命令 首先,登陆mysql ? 1 mysql -u root -p 然后执行set password命令 ? 1 set password for root@loc ...

  3. solr排序问题

         搜搜引擎排序问题,因为涉及到的维度比较多,有时候单纯的依靠sort是无法满足需要的,例如:搜索商品的时候我希望不管怎么排无货的商品都置底,这样问题就来了,怎么排? 其实,solr是自己的解决 ...

  4. iPhone 尺寸

    http://tool.lanrentuku.com/guifan/ui.html 这是本人复制的链接,,

  5. Git 深入浅出

    如果你是一个开发人员,想用上这个世界上目前最先进的分布式版本控制系统,那么,赶快开始学习吧!(耐心读下去,收获满满) Git是什么? Git是目前世界上最先进的分布式版本控制系统(没有之一). Git ...

  6. pwntools使用简介2

    大致框架 官网的一个简单样例 from pwn import * context(arch = 'i386', os = 'linux') r = remote() # EXPLOIT CODE GO ...

  7. python---scrapy之MySQL同步存储

    假设我们已经能获取到item里定义的字段的数据,接下来就需要保存item的数据到mysql数据库. pipeline用来存储item中的数据,将爬取到的数据进行二次处理 首先,要做的准备的工作,安装M ...

  8. NI笔试——大数加法

    NI笔试: 1.找出字符串第一次出现的字符.用数组建立哈希表,然后再扫描字符串并判断次数是否为1. 2.大数加法,即字符串加法.因为之前写过乘法,就以为是乘法.然后就把乘法写上去了····= = 好了 ...

  9. 安卓开发中Spinner控件的使用

    在安卓手机应用开发中,Spinner对象常用方法有以下五种. 用法 1 :以资源方式,静态展示 Spinner 选项 用法 2 :以代码方式,动态展示 Spinner 选项 用法 3 :同时显示图片和 ...

  10. [js高手之路] dom常用节点属性兼容性详解与应用

    一.每个DOM节点都有一个nodeType属性,表示节点类型, NodeType一共有12种类型,我们可以通过遍历内置的Node构造函数获取 window.onload = function(){ v ...