本节我们展示如何在组件中获取一个`YzAsyncValidators`实例。
## 手动构建
`YzAsyncValidators`的构造函数中声明了`HttpClient`,也就是构造一个`YzAsyncValidators`实例的前提是需要一个`HttpClient`实例。
```typescript
/**
* 异步验证器.
*/
export class YzAsyncValidators {
constructor(private httpClient: HttpClient) {
}
```
在学生添加组件中,可以采取自动注入的方式,轻构的获取到一个`HttpClient`实例:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
- constructor() {
+ constructor(private httpClient: HttpClient) {
```
然后就可以使用这个`HttpClient`实例来构造一个`YzAsyncValidators`实例了:
```typescript
constructor(private httpClient: HttpClient) {
- const yzAsyncValidators = null as unknown as YzAsyncValidators;
+ const yzAsyncValidators = new YzAsyncValidators(httpClient);
this.formGroup = new FormGroup({
```
执行学生新增组件的单元测试,错误信息消失,当输入学号时,在控制台中打印信息如下:
![image-20210414085521792](https://img.kancloud.cn/8a/ea/8aea5d10999e9ac8895f2f683f4b4283_1082x152.png)
## 完成功能
`YzAsyncValidators`有了`HttpClient`后,便可以向后台发起请求,然后根据后台的返回值情况来决定验证是否通过。
```typescript
+++ b/first-app/src/app/yz-async-validators.ts
numberNotExist(): (control: AbstractControl) => Observable<ValidationErrors | null> {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
- console.log(this.httpClient);
- console.log('异步验证器被调用');
- return of(null)
- .pipe(delay(1000), tap(data => console.log('验证器返回数据', data)));
+ const httpParams = new HttpParams()
+ .append('number', control.value); ①
+ return this.httpClient.get<boolean>③('/student/numberIsExist', {params: httpParams}) ②
+ .pipe④(map⑤(exists => exists ? {numberExist: true} : null)⑥);
};
}
}
```
- ① 创建了请求参数,并将`number`加入请求参数中
- ② 发起了一个加入了请求参数的**预**请求,该**预**请求仅当被订阅时才会发起真正的请求,而`FormControl`的异步验证器会根据情况来决定是否进行订阅,从而控制是否向后台发起请求。
- ③ 该预请求的返回值的类型为`boolean`,这是由后面台API返回类型决定的。所以预请求的返回值类型为`Observable<boolean>`
- ④ `pipe`的作用是使数据依次通过各个管道(操作符)。
- ⑤ `map`操作符能起到数据转换的作用。`boolean`类型的值由管道这头输入,然后它按自己的规则决定管道那头的输出。
- ⑥ 当前的转换规则是:如果管道这头接收的是`true`,则管道那头输出`{numberExist: true}`;如果管道这头接收提`false`,则管道那头输出的是`null`。该规则将`boolean`类型的值成功的转换为了`ValidationErrors | null`。便得最终的返回值类型由`Observable<boolean>`变更为` Observable<ValidationErrors | null>`
为了更好的理解`map`操作符,我们在当前的学生增加组件的测试文件中增加一个测试方法:
```typescript
+++ b/first-app/src/app/student/add/add.component.spec.ts
fit('理解map操作符', () => {
// 数据源发送数据1
const a = of(1) as Observable<number>;
a.subscribe(data => console.log(data));
// 数据源还是发送数据1
const b = of(1) as Observable<number>;
// 使用pipe,但不加任何管道
const c = b.pipe() as Observable<number>;
c.subscribe(data => console.log(data));
// 接着发送数据源1,通过map操作符完成number到string的转换
const d = of(1) as Observable<number>;
const e = d.pipe(map(data => {
console.log('map操作符接收到的值的类型为:', typeof data);
const result = data.toString();
console.log('map操作符转换后的值的类型为:', typeof result);
return result;
})) as Observable<string>;
e.subscribe(data => console.log(data));
// 下面的写法与上面的效果一样,当箭头函数中仅有一行代码时,可以省略该代码的return关键字
const f = of(1) as Observable<number>;
const g = f.pipe(map(data => data.toString())) as Observable<string>;
g.subscribe(data => console.log(data));
// 除进行一般的类型转换外,还可以转换为任意类型
const h = of(1).pipe(map(data => {
return {value: data};
})) as Observable<{ value: number }>;
h.subscribe(data => console.log(data));
// 当然也可以转换为null类型
const i = of(1).pipe(map(data => {
if (data !== 1) {
return {test: data};
} else {
return null;
}
})) as Observable<{ value: number } | null>;
i.subscribe(data => console.log(data));
});
```
测试结果如下:
![image-20210414092934339](https://img.kancloud.cn/9c/6e/9c6eeae122617b27936328b60efe62ed_1398x264.png)
## 测试
接着回到学号的异步验证器的测试上来:当输入032282时错误时显示错误提示。
![image-20210414093034719](https://img.kancloud.cn/47/ce/47ce3abe7d9f2d22115cb10ad1ebd0ce_1236x190.png)
当输入其它学号时,错误提示消失:
![image-20210414093223995](https://img.kancloud.cn/4a/4d/4a4d0ce1e0014c8f0a0f9eff9d3e8d55_1180x146.png)
上述测试足以说明异步验证是有效的。
## 完善提示信息
输入032282的学号时,异步验证器返回了验证失效的信息,但提示信息却为:学号不能为空。正确的信息应该是:该学号已存在。
当一个`FormControl`存在多个验证器时,当验证不通过时可以通过`FormControl`的`errors`来定制验证信息:
```html
+++ b/first-app/src/app/student/add/add.component.html
@@ -11,7 +11,7 @@
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">学号</label>
<div class="col-sm-10">
- {{formGroup.get('number').pending}}
+ {{formGroup.get('number').errors | json}}
<input type="text" class="form-control" formControlName="number">
<small class="text-danger" *ngIf="formGroup.get('number').invalid">
学号不能为空
```
此时,当学号为空时,显示的提示信息如下:
![image-20210414093606246](https://img.kancloud.cn/2a/69/2a69b6e194576c3a169b213192e79dd9_1182x180.png)
当学号为032282时,显示的提示信息为:
![image-20210414093628734](https://img.kancloud.cn/9f/95/9f95c3197bab8000ce294782bf4da3d0_1154x188.png)
如此以来,便可以利用`errors`如下定义提示信息了:
```html
<small class="text-danger" *ngIf="formGroup.get('number').invalid">
- 学号不能为空
+ <span *ngIf="formGroup.get('number').errors.required">学号不能为空</span>
+ <span *ngIf="formGroup.get('number').errors.numberExist">该学号已存在</span>
</small>
```
此时将输入032282时,将提示**该学号已存在**。
![image-20210414093947507](https://img.kancloud.cn/49/ea/49ea9e285161fe6de4c53e7f75514b5c_1132x180.png)
提示信息解决后,还需要解决异步验证器在发起后台请求时保存按钮被点亮的BUG。解决该问题可以利用`FormControl`的`pending`属性:
```html
<div class="col-sm-10 offset-2">
- <button class="btn btn-primary" [disabled]="formGroup.invalid">保存
+ <button class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">保存
</button>
</div>
```
如此以来,当异步验证器发起异步验证时`pending`的值为`true`,保存按钮不可用;当异步验证器完成异步验证时`pending`的值为`false`,此时保存按钮是否可以则取决于`invalid`属性。
## 自动构建
完善完异步验证器后,让我们把思绪拉回来在组件中构造`YzAsyncValidators`实例上来。刚刚我们通过在组件中注入`HttpClient`,然后再构建 `YzAsyncValidators`实例。
但随着项目的更新,上述方法可能会引发比较严重的问题。
![image-20210414094710308](https://img.kancloud.cn/e6/ab/e6abeb7aec21b3383cd16f8a2dd5b375_846x272.png)
1. 组件a、b、c、d、e全部依赖于`YzAsyncValidators`验证器。所以在组件a、b、c、d、e全部注入`HttpClient`实例,进而使用`new `YzAsyncValidators(httpClient)`来获取一个验证器实例。
2. 有一天`YzAsyncValidators`除依赖于`HttpClient`外,还依赖于`ActivedRouter`实例,构造函数变更为:`constructor(private httpClient: HttpClient, private activedRouter: ActivedRouter) {`。
3. 这时候灾难便发生了,我们需要依次修改组件a、b、c、d、e,每个组件中都需要再注入个`ActivedRouter`实例。
这明显是不可接受的,我们的理想目标是当`YzAsyncValidators`变更时,依赖于该`YzAsyncValidators`组件可以在不进行任何变更的情况下继续正常工作。
预解决这个问题,便需要将手动实例化`YzAsyncValidators`改为自动注入了:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -12,8 +12,7 @@ import {HttpClient} from '@angular/common/http';
export class AddComponent implements OnInit {
formGroup: FormGroup;
- constructor(private httpClient: HttpClient) {
- const yzAsyncValidators = new YzAsyncValidators(httpClient);
+ constructor(private httpClient: HttpClient, private yzAsyncValidators: YzAsyncValidators) {
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()),
```
此时当前测试将发生一个错误:
![image-20210414095513624](https://img.kancloud.cn/40/e8/40e8b1831b6d2b208efcd66bc6c12a1a_1706x148.png)
该错误提示我们说:没有找到`YzAsyncValidators`的提供者。没错,由于我们未对`YzAsyncValidators`类进行任何的配置,所以当前动态测试模块中并没有提供`YzAsyncValidators`的能力。这时候就需要复习一下6.7.2中的单例服务了:
![image-20210409180550615](https://img.kancloud.cn/ec/b1/ecb134829c80e95e8b65b862ec9e168c_1864x392.png)
上图的`AuthService`只所以能够被Angular注入到任意的模块中,是由于`AuthService`并声明在`root`模块中。按上述思想,我们将`YzAsyncValidators`也声明到`root`模块中:
```typescript
+++ b/first-app/src/app/yz-async-validators.ts
@@ -2,10 +2,14 @@ import {AbstractControl, ValidationErrors} from '@angular/forms';
import {Observable, of} from 'rxjs';
import {delay, map, tap} from 'rxjs/operators';
import {HttpClient, HttpParams} from '@angular/common/http';
+import {Injectable} from '@angular/core';
/**
* 异步验证器.
*/
+@Injectable({
+ providedIn: 'root'
+})
export class YzAsyncValidators {
constructor(private httpClient: HttpClient) {
```
此时Anguar便有了提供`YzAsyncValidators`的能力,单元测试中的错误随即消失,异步验证器工作正常。
![image-20210414093947507](https://img.kancloud.cn/49/ea/49ea9e285161fe6de4c53e7f75514b5c_1132x180.png)
此时,即使有一天我们变更`YzAsyncValidators`的依赖,依赖于该`YzAsyncValidators`组件也可以在不进行任何变更的情况下继续正常工作。
| 名称 | 链接 |
| --------------- | ------------------------------------------------------------ |
| Angular依赖注入 | [https://angular.cn/guide/dependency-injection](https://angular.cn/guide/dependency-injection) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.2.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.2.4.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结