使用confirm及alert来进行弹窗提醒虽然方便,但浏览器为我们提供的弹窗着实欠缺一些友好性。本节来实现一个看起来更漂亮的弹窗。
# 实现原理
人类文明中充满着各种"trick 戏法",比如你在电脑上看电影时其实是在观看一张张快速切换的照片,由于这些照片播放的太快了使得我们就像看到了真实的物体在变化一样;再比如各种第一视角的游戏(比如绝地求生),其实画面中的"你"一直都是位于屏幕中央的位置而从未动过,由于"你"所处的周边环境的画面在变化而使得你感觉就像自己在动一下;再比如被点亮的灯泡的亮度其实一在不停地变化,只是由于变化的频率太快,从而使得你感觉其亮度是一直不变的而已。
弹窗的实现,也是一种"trick 戏法"。
# 图层
如果你有一些PS基础,或使用过其它一些图片(视频)编辑软件,对这个概念一定不会陌生。在进行图片处理时,会将多个图层由上至下排列,图片的最终效果则是这些图片由上及下的合成效果。
![](https://img.kancloud.cn/b2/f6/b2f676f44d06fcc2f1d0fa6a6672ff9f_429x387.png)
比如下图便是按上面的理论使用制图软件绘制的一条小鱼:
![](https://img.kancloud.cn/da/77/da774b18486f017b4ef54be76c79cfed_618x317.png)
其实视频软件中像"添加字幕"、"画中画"的功能也是类似。如果向上追溯的话,笔者在上世纪90年代读初中时,曾经接触过一种叫做幻灯机的东西,至令印象颇深。
![](https://img.kancloud.cn/bc/80/bc80787996282316e3843e6cd9777c4c_288x377.png)
>[info] PPT又被称为"幻灯片"。^_^,你想到了什么?
它的原理如下:
![](https://img.kancloud.cn/67/ce/67ceddefb9feb588c7e6bc289a1f46a0_465x420.png)
再往上追溯还可以追溯到我国的传统艺术:皮影。
CSS中其实也有"图层"的概念,它在CSS中的名字叫:position 定位。
# 弹窗原理
![](https://img.kancloud.cn/67/8b/678bf4bf52c1af3e47f1abbf5d174ee3_346x280.gif)
# 实践
有了原理以后,开始分步尝试开发,拟分为以下几步:
1. 建立两个div。其中一个起半透明的遮罩层的作用;第二个用于定制弹出窗口
2. 用户点击删除时,显示这两个DIV。
3. 定制第一个DIV,完成其半透明的遮罩层功能。
4. 定制第二个DIV,使其显示在遮罩层上方,并且居中显示
5. 给第二个DIV添加一个说明,一个确认按钮,一个取消按钮
6. 给确认按钮及取消按钮分别加入对应的功能
7. 集成测试
## 建立DIV
开启集成测试模式,并来到学生管理界面。找到对应的V层,新建两个DIV。
src/app/student/index/index.component.html
```html
<div>这是遮罩层</div>
<div>这是弹出窗口</div>
<form (ngSubmit)="onQuery()">
...
```
![](https://img.kancloud.cn/c8/88/c8887932be6de935dd7d451c1bd07741_556x196.png)
## 用户删除时显示DIV
首先将其设置为默认隐藏。
src/app/student/index/index.component.html
```html
<div *ngIf="showPopWindow">这是遮罩层</div>
<div *ngIf="showPopWindow">这是弹出窗口</div>
<form (ngSubmit)="onQuery()">
```
src/app/student/index/index.component.ts
```javascript
export class IndexComponent implements OnInit {
...
showPopWindow = false;
```
接着当用户点击删除时,设置`showPopWindow`的值为true,同时为了避免发起真实的删除操作,在删除方法中暂时添加return语句。
src/app/student/index/index.component.ts
```javascript
onDelete(student: Student): void {
this.showPopWindow = true;
return;
const result = confirm('这里是提示的消息');
...
```
![](https://img.kancloud.cn/03/86/03861a466768a9b0eb24cb0c2bce25b4_1052x303.gif)
## 定制遮罩层
遮罩层有以下个特点:
* 位于主体窗口之上
* 大小与浏览器窗口相同
* 有个灰色的背景该背景透明
而以上几个特点则都是由CSS来控制实现的,实现如下:
设置class
src/app/student/index/index.component.html
```html
<div *ngIf="showPopWindow" class="mask">这是遮罩层</div>
...
```
src/app/student/index/index.component.sass
```sass
...
.mask
position: fixed
background-color: green
```
* 使用position: fixed将该div设置为新的图层
* 设置个背景色以在开发过程中观察该DIV的大小
测试:
![](https://img.kancloud.cn/f7/df/f7df7bc27cbb15e04356838dfaaeb8b2_360x194.png)
此时:遮罩层遮挡住了"这是弹出窗口"所在的DIV,表明其位于主体窗口之上成功。但大小不符合要求,继续设置如下:
src/app/student/index/index.component.sass
```sass
...
.mask
position: fixed
background-color: green
height: 100%
width: 100%
```
测试:
![](https://img.kancloud.cn/17/09/170999e9f874a7cb333d2f49bd9ec3f4_1428x396.png)
* 该DIV的起始位置处于原位置(未设置fixed属性前的位置),导致未能占满整个屏幕。
* 有些元素位于遮罩层之上
问题一,使用top,left自定义该图层距离浏览器上方及左侧的距离:
src/app/student/index/index.component.sass
```sass
...
.mask
position: fixed
background-color: green
height: 100%
width: 100%
top: 0px
left: 0px
```
问题二:各个图层(position: fixed)将z-index的值由大到小,进行由上到下排列,当图层的z-index值相同时按后出现的图层排到之前图层之上。所以,解决该问题的方法是:将mask的index设置为一个较大的值。
src/app/student/index/index.component.sass
```sass
...
.mask
position: fixed
background-color: green
height: 100%
width: 100%
top: 0px
left: 0px
z-index: 1000
```
测试通过。
## 使弹出窗口位于遮罩层上方
有了刚刚的经验这个就不太难了,实现代码如下:
src/app/student/index/index.component.html
```html
<div *ngIf="showPopWindow" class="mask">这是遮罩层</div>
<div *ngIf="showPopWindow" class="popWindow">这是弹出窗口</div>
<form (ngSubmit)="onQuery()">
```
src/app/student/index/index.component.sass
```sass
.popWindow
position: fixed
z-index: 1001
```
## 弹窗样式
src/app/student/index/index.component.sass
```sass
.popWindow
position: fixed
top: 50%
left: 50%
z-index: 1001
```
* 将top与left设置为50%,以达到居中的目的
![](https://img.kancloud.cn/cb/56/cb56d62c2dc64076aac4b6cece8dada3_1182x344.png)
## 添加说明、按钮细化样式
src/app/student/index/index.component.html
```html
<div *ngIf="showPopWindow" class="mask">这是遮罩层</div>
<div *ngIf="showPopWindow" class="popWindow">
<h5>这里是弹窗说明</h5>
<hr>
<div class="text-right">
<button class="btn btn-sm btn-warning">取消</button>
<button class="btn btn-sm btn-primary">确认</button>
</div>
</div>
<form (ngSubmit)="onQuery()">
```
细化样式如下:
src/app/student/index/index.component.sass
```sass
.popWindow
position: fixed
width: 300px
min-height: 140px
top: calc(50% - 70px)
left: calc(50% - 150px)
z-index: 1001
background-color: aliceblue
padding: 20px 20px 10px
.popWindow h5
min-height: 50px
.popWindow button
margin: auto 8px
```
* 使用calc运算符将弹窗进行居中。请思索:为什么要分别减70px及150px
其它的属性请依次添加后分步查看添加后效果,最终效果如下:
![](https://img.kancloud.cn/bb/ad/bbad0adbe0892e34608c48471678c564_977x369.png)
## 加入对应的功能
实现删除的方法有很多种,在此给出实现简单的一种,具体的流程如下:
![](https://img.kancloud.cn/87/f2/87f243b42df0af756221c766a471fda8_421x498.png)
按此流程,依次完善V层及C层代码:
src/app/student/index/index.component.html
```
<div class="text-right">
<button class="btn btn-sm btn-warning" type="button" (click)="cancel()">取消</button>
<button class="btn btn-sm btn-primary" type="button" (click)="confirm()">确认</button>
</div>
```
src/app/student/index/index.component.ts
```javascript
/*缓存要删除的学生*/
cacheDeleteStudent: Student;
/**
* 删除学生
* @param student 学生
*/
onDelete(student: Student): void {
this.cacheDeleteStudent = student;
this.showPopWindow = true;
}
/**
* 删除缓存的学生后,隐藏弹窗
*/
deleteCacheStudent() {
const student = this.cacheDeleteStudent;
this.studentService.deleteById(student.id)
.subscribe(() => {
this.pageStudent.content.forEach((value, key) => {
if (value === student) {
this.pageStudent.content.splice(key, 1);
}
});
});
}
/**
* 点击确认
*/
confirm() {
this.deleteCacheStudent();
this.showPopWindow = false;
}
/**
* 点击取消
*/
cancel() {
this.showPopWindow = false;
}
```
## 集成测试
![](https://img.kancloud.cn/e7/0f/e70f7d47fe29bb9e2778701a8bcd0c6a_1391x337.gif)
集成测试过程中发现以下问题:
1. 弹窗说明的文字为:这里是弹窗说明。正确的应该改为:请您再次确认
2. 遮罩层的背景颜色为绿色。正确的应为灰色(介于黑与白之间)
3. 遮罩层没有半透明设置。正确的应为半透明
4. 左上角遮罩层有测试文字:这是遮罩层。正确的应该没有文字。
对于1,4两点请自行修正。对于2,3两点修正如下:
src/app/student/index/index.component.sass
```sass
.mask
position: fixed
background-color: gray ➊
height: 100%
width: 100%
top: 0px
left: 0px
z-index: 1000
opacity: 80% ➋
```
* ➊ 背景色灰色
* ➋ 不透明度80%
最终效果如下:
![](https://img.kancloud.cn/db/ca/dbcac90666aeb19816a34b269056fd9a_1391x337.gif)
## 单元测试
在加入功能的环节中,我们:增加了3个方法,修改了1个方法。对应增加3个测试用例如下:
src/app/student/index/index.component.spec.ts
```javascript
fit('deleteCacheStudent', () => {
});
fit('confirm', () => {
});
fit('cancel', () => {
});
```
### deleteCacheStudent
此方法实际上是将原onDelete方法的部分逻辑进行迁移,在原来onDelete方法的基础上稍做修正:
src/app/student/index/index.component.spec.ts
```javascript
fit('deleteCacheStudent', () => {
// 替身及模似数据的准备
const studentService = TestBed.get(StudentService);
const subject = new BehaviorSubject<void>(undefined);
spyOn(studentService, 'deleteById').and.returnValue(subject);
// 调用方法,删除第一个学生
const student = component.pageStudent.content[0];
component.cacheDeleteStudent = student; // ➊
component.deleteCacheStudent();
// 断言删除的学生成功的由前台移除
let found = false;
component.pageStudent.content.forEach(value => {
if (value === student) {
found = true;
}
});
expect(found).toBeFalsy();
});
```
* ➊ 设置缓存的要删除的学生
### confirm
src/app/student/index/index.component.spec.ts
```javascript
fit('confirm', () => {
// 替身及数据准备
spyOn(component, 'deleteCacheStudent');
component.showPopWindow = true;
// 调用
component.confirm();
// 断言
expect(component.showPopWindow).toBeFalsy();
expect(component.deleteCacheStudent).toHaveBeenCalled();
});
```
### cancel
src/app/student/index/index.component.spec.ts
```javascript
fit('cancel', () => {
// 替身及数据准备
component.showPopWindow = true;
// 调用
component.cancel();
// 断言
expect(component.showPopWindow).toBeFalsy();
});
```
### onDelete
src/app/student/index/index.component.spec.ts
```javascript
it('onDelete -> 确认删除', () => {
// 替身及模似数据的准备
component.showPopWindow = false;
const student = new Student();
// 调用
component.onDelete(student);
// 断言
expect(component.cacheDeleteStudent).toBeTruthy(student);
expect(component.showPopWindow).toBeTruthy();
});
```
最后,将所有的`f`去除,做全局测试:
错误一:
![](https://img.kancloud.cn/51/43/5143d1e98ffa4390a2bb05d75c03aef1_850x143.png)
错误原因:只有单独进行某个单元测试时,才可以使用root1根选择器。在多个单元测试共同进行时,angular会为每个单元测试生成唯一的root编号,比如:root2,root3,root4等。
修正如下:
src/app/student/index/index.component.spec.ts
```javascript
FormTest.clickButton(fixture, '#root1 > table > tr:nth-child(2) > td:nth-child(6) > button'); ✘
FormTest.clickButton(fixture, 'table > tr:nth-child(2) > td:nth-child(6) > button'); ✚
```
错误二:
![](https://img.kancloud.cn/a7/2b/a72b1f5c7aaa39ee2acb6aac35ed7835_422x129.png)
找到对应的测试用例,将`it`变更为`fit`,再次测试:
![](https://img.kancloud.cn/c3/86/c386d9eec1bc6bad07ae8f8b37d2db46_626x467.png)
排查看确认是由于校验规则失效导致C层对应的`submit`方法未生效,修正单元测试如下:
src/app/student/edit/edit.component.spec.ts
```javascript
fit('点击保存按钮', () => {
spyOn(component, 'onSubmit');
component.formGroup.get('name').setValue('123'); ✚
component.formGroup.get('sno').setValue('123421'); ✚
fixture.detectChanges(); ✚
const button: HTMLButtonElement = fixture.debugElement.query(By.css('button')).nativeElement;
button.click();
expect(component.onSubmit).toHaveBeenCalled();
});
```
>[success] 单元测试的职责正是如此。当某些逻辑变更时,历史的单元测试会及时的发出警告信息。
修正后单元测试全部通过,保证了未因增加新功能而对历史功能的造成影响。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.5) | - |
| CSS Position(定位) | [https://www.runoob.com/css/css-positioning.html](https://www.runoob.com/css/css-positioning.html) | 5 |
| CSS z-index 属性 | [https://www.runoob.com/cssref/pr-pos-z-index.html](https://www.runoob.com/cssref/pr-pos-z-index.html) | 5 |
| CSS3 opacity 属性 | [https://www.runoob.com/cssref/css3-pr-opacity.html](https://www.runoob.com/cssref/css3-pr-opacity.html) | 5 |
| CSS calc() 函数 | [https://www.runoob.com/cssref/func-calc.html](https://www.runoob.com/cssref/func-calc.html) | 5 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用