本节我们展示如何在组件中获取一个`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) |