本節(jié)我們?yōu)樾略鼋M件增加一些驗(yàn)證。在正式編碼之前,為了使用組件與我們有個(gè)更好的交互,在單元測試的代碼中的最后一行啟動(dòng)自動(dòng)檢測變更:
```typescript
+++ b/first-app/src/app/student/add/add.component.spec.ts
@@ -32,5 +32,7 @@ describe('student -> AddComponent', () => {
expect(component).toBeTruthy();
getTestScheduler().flush();
fixture.detectChanges();
+
+ fixture.autoDetectChanges();
});
});
```
## 加入驗(yàn)證器
然后給名稱、學(xué)號、班級分別加一個(gè)`required`驗(yàn)證器:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -1,5 +1,5 @@
import {Component, OnInit} from '@angular/core';
-import {FormControl, FormGroup} from '@angular/forms';
+import {FormControl, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'app-add',
@@ -8,11 +8,11 @@ import {FormControl, FormGroup} from '@angular/forms';
})
export class AddComponent implements OnInit {
formGroup = new FormGroup({
- name: new FormControl(),
- number: new FormControl(),
+ name: new FormControl('', Validators.required),
+ number: new FormControl('', Validators.required),
phone: new FormControl(),
email: new FormControl(),
- clazzId: new FormControl()
+ clazzId: new FormControl(null, Validators.required)
});
constructor() {
```
測試一校驗(yàn)正常。
## 自定義驗(yàn)證器
一般情況下,只驗(yàn)證是否為空是滿足不了實(shí)際的需求的,比如我們在這對手機(jī)號的驗(yàn)證、郵箱的驗(yàn)證。Angular支持自定義`FormControl`驗(yàn)證器,來幫助我們實(shí)現(xiàn)Angular內(nèi)置驗(yàn)證器無法滿足的需求。比如當(dāng)前需要一定驗(yàn)證手機(jī)號是否正確的驗(yàn)證器。
自定義驗(yàn)證器與自定義`FormControl`有著相通之處:都需要符合某種規(guī)范。自定義`FormControl`使用了實(shí)現(xiàn)接口來達(dá)到符合規(guī)范的目的,自定義驗(yàn)證器使用返回特定類型來達(dá)到符合規(guī)范的目的。
在自定義驗(yàn)證器時(shí),我們也像Angular一樣,把所有的驗(yàn)證器放到一個(gè)類中。同時(shí)由于表單驗(yàn)證器可供所有的項(xiàng)目使用,所以我們將其建立在項(xiàng)目的根目錄下,至于名字我們將其命名為`YzValidators`:
```bash
panjie@panjies-iMac app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app
panjie@panjies-iMac app % ng g class YzValidators
CREATE src/app/yz-validators.spec.ts (179 bytes)
CREATE src/app/yz-validators.ts (30 bytes)
```
空的類文件`YzValidators`如下所示:
```typescript
export class YzValidators {
}
```
### 驗(yàn)證手機(jī)號
然后我們在該方法中建立一個(gè)用于手機(jī)號驗(yàn)證的靜態(tài)方法`phone`:
```typescript
import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
export class YzValidators {
/**
* 驗(yàn)證手機(jī)號
*/
??static phone(control: AbstractControl①): ValidationErrors② | null {
return null; ③
}
}
```
- ??方法以`static`關(guān)鍵字標(biāo)識(shí)
- 該方法將當(dāng)前`FormControl`做為參數(shù)傳入,所以①處的類型必須是`AbstractControl`
- 該方法的返回值類型必須是`ValidationErrors | null`,該返回值類型決定了其是一個(gè)表單驗(yàn)證器
- ③當(dāng)驗(yàn)證通過時(shí),將返回nulll;驗(yàn)證不通過時(shí),將返回`ValidationErrors`類型的數(shù)據(jù)
> 實(shí)際上`phone()`方法是`ValidatorFn`的實(shí)現(xiàn)。
### 應(yīng)用驗(yàn)證器
自定義驗(yàn)證器的用法與Angular內(nèi)置驗(yàn)證器的用法完全相同:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -11,7 +11,7 @@ export class AddComponent implements OnInit {
formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required),
- phone: new FormControl(),
+ phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
});
```
在V層中加入測試的代碼:
```html
<div class="col-sm-10">
<input type="text" class="form-control" formControlName="phone">
{{formGroup.get('phone').invalid}}
<small class="text-danger" *ngIf="formGroup.get('phone').invalid">
手機(jī)號格式不正確
</small>
</div>
```
此時(shí)由于驗(yàn)證方法`phone()`的返回值為null,所以無論在手機(jī)號一欄中輸入什么內(nèi)容,都不會(huì)報(bào)錯(cuò):

假設(shè)把返回值改成非null,則無論輸入的什么樣的值該驗(yàn)證器都會(huì)報(bào)錯(cuò):
```typescript
static phone(control: AbstractControl): ValidationErrors | null {
return {phone: '手機(jī)號格式錯(cuò)誤'};
}
```

## 調(diào)用時(shí)機(jī)
每當(dāng)`FormControl`有數(shù)據(jù)變更時(shí),對應(yīng)的驗(yàn)證器都會(huì)被執(zhí)行一次:
```typescript
static phone(control: AbstractControl): ValidationErrors | null {
console.log('phone is called');
return {phone: '手機(jī)號格式錯(cuò)誤'};
}
```
此時(shí)當(dāng)我們變更表單中的手機(jī)號時(shí),在控制臺(tái)中將反復(fù)打印相同的日志:

## 完成功能
既然每次數(shù)據(jù)改變都會(huì)調(diào)用一次`phone()`方法,那么在`phone()`方法中我們獲取當(dāng)前的手機(jī)號,然后使用相關(guān)的方法驗(yàn)證手機(jī)號是否符合要求即可:
```typescript
static phone(control: AbstractControl): ValidationErrors | null {
const phone = control.value as string;
// 如果手機(jī)號是11位,并且以1打頭,則驗(yàn)證成功
if (phone.length === 11 && phone.startsWith('1')) {
return null;
}
return {phone: '手機(jī)號格式錯(cuò)誤'};
}
```
如此以來,當(dāng)輸入11位以1打頭的手機(jī)號時(shí),驗(yàn)證成功;

否則驗(yàn)證失?。?

## 單元測試
我們剛剛制定了一個(gè)規(guī)則相關(guān)粗暴的驗(yàn)證規(guī)則,相信在生產(chǎn)項(xiàng)目中你如上驗(yàn)證手機(jī)號是否合規(guī)則一定離被噴不遠(yuǎn)了。簡單粗暴的方案使得測試也比較簡單,這時(shí)候完成可用使用**手動(dòng)**的方法。但如果測試的條件更多的話**手動(dòng)**的方法便不切合實(shí)際了。
試想一下我們每完成一些驗(yàn)證手機(jī)號的代碼,就需要從130-189號段一個(gè)個(gè)驗(yàn)證一遍,那將是什么樣的痛苦。這時(shí)候就顯出單元測試的優(yōu)勢了。
對于當(dāng)前的驗(yàn)證,在掌握了自定義驗(yàn)證器的基本方法后,完全可以如下定制單元測試:
```typescript
describe('YzValidators', () => {
fit('should create an instance', () => {
expect(new YzValidators()).toBeTruthy();
// 空手機(jī)號,返回非null
const formControl = new FormControl('');
expect(YzValidators.phone(formControl)).toBeTruthy();
// 正常的手機(jī)號,返回null
formControl.setValue('13900000000');
expect(YzValidators.phone(formControl)).toBeNull();
// 以2打頭,返回非null
formControl.setValue('23900000000');
expect(YzValidators.phone(formControl)).toBeTruthy();
// 不足11位,返回非null
formControl.setValue('1390000000');
expect(YzValidators.phone(formControl)).toBeTruthy();
});
});
```
或者,直接將驗(yàn)證器引入`FormControl`:
```typescript
+++ b/first-app/src/app/yz-validators.spec.ts
@@ -2,7 +2,7 @@ import { YzValidators } from './yz-validators';
import {FormControl} from '@angular/forms';
describe('YzValidators', () => {
- fit('should create an instance', () => {
+ it('should create an instance', () => {
expect(new YzValidators()).toBeTruthy();
// 空手機(jī)號,返回非null
const formControl = new FormControl('');
@@ -20,4 +20,22 @@ describe('YzValidators', () => {
formControl.setValue('1390000000');
expect(YzValidators.phone(formControl)).toBeTruthy();
});
+
+ fit('將驗(yàn)證器加入到FromControl', () => {
+ // 空手機(jī)號,校驗(yàn)失敗
+ const formControl = new FormControl('', YzValidators.phone);
+ expect(formControl.invalid).toBeTrue();
+
+ // 正常的手機(jī)號,校驗(yàn)成功
+ formControl.setValue('13900000000');
+ expect(formControl.invalid).toBeFalse();
+
+ // 以2打頭,校驗(yàn)失敗
+ formControl.setValue('23900000000');
+ expect(formControl.invalid).toBeTrue();
+
+ // 不足11位,校驗(yàn)失敗
+ formControl.setValue('1390000000');
+ expect(formControl.invalid).toBeTrue();
+ });
});
```
此外,我們還可以使用`valid`字段來替換`invalid`字段,這樣以來成功的時(shí)候斷言為`true`,失敗時(shí)斷言為`false`更易懂一些:
```typescript
+++ b/first-app/src/app/yz-validators.spec.ts
@@ -25,17 +25,21 @@ describe('YzValidators', () => {
// 空手機(jī)號,校驗(yàn)失敗
const formControl = new FormControl('', YzValidators.phone);
expect(formControl.invalid).toBeTrue();
+ expect(formControl.valid).toBeFalse();
// 正常的手機(jī)號,校驗(yàn)成功
formControl.setValue('13900000000');
expect(formControl.invalid).toBeFalse();
+ expect(formControl.valid).toBeTrue();
// 以2打頭,校驗(yàn)失敗
formControl.setValue('23900000000');
expect(formControl.invalid).toBeTrue();
+ expect(formControl.valid).toBeFalse();
// 不足11位,校驗(yàn)失敗
formControl.setValue('1390000000');
expect(formControl.invalid).toBeTrue();
+ expect(formControl.valid).toBeFalse();
});
});
```
如此以來,當(dāng)我們開發(fā)更加適用的手機(jī)驗(yàn)證器時(shí),便可以維護(hù)一個(gè)更加符合現(xiàn)實(shí)的單元測試列表。然后在開發(fā)過程中,自動(dòng)執(zhí)行該測試文件,當(dāng)所有的測試都通過時(shí),就說明自己的功能成功了。相比于**手動(dòng)**測試,這種單元測試的方法即安全、又高效。
## 總結(jié)
自定義驗(yàn)證器很簡單,只需要將方法聲明為`static`,并將方法的返回值類型聲明為` ValidationErrors | null`即可。
- 如果驗(yàn)證內(nèi)容通過,則返回`null`。
- 如果驗(yàn)證內(nèi)容沒有通過,則返回` ValidationErrors`
- `FormControl`每變更一次,該驗(yàn)證方法執(zhí)行一次,所以在該方法中的代碼一定要是高效的。
` ValidationErrors`類型實(shí)際上是以`字符串`類型為鍵值的對象,比如:`{hello: 123}`、`{a: 123}`等都是其合法的值。
```typescript
export declare type ValidationErrors = {
[key: string]: any;
};
```
返回的錯(cuò)誤信息最終將匯總到`FormControl`的`errors`字段中:
```html
+++ b/first-app/src/app/student/add/add.component.html
@@ -22,6 +22,7 @@
<div class="col-sm-10">
<input type="text" class="form-control" formControlName="phone">
{{formGroup.get('phone').invalid}}
+ {{formGroup.get('phone').errors | json}}
<small class="text-danger" *ngIf="formGroup.get('phone').invalid">
手機(jī)號格式不正確
</small>
```

## 本節(jié)作業(yè)
嘗試完成用于驗(yàn)證郵箱是否合法的郵箱驗(yàn)證器,比如:
```typescript
static email(control: AbstractControl): ValidationErrors | null {
// 這里是邏輯實(shí)現(xiàn)
}
```
然后將此驗(yàn)證器應(yīng)用到郵箱字段上,并進(jìn)行充分的測試。
| 名稱 | 鏈接 |
| ---------------- | ------------------------------------------------------------ |
| 定義自定義驗(yàn)證器 | [https://angular.cn/guide/form-validation#defining-custom-validators](https://angular.cn/guide/form-validation#defining-custom-validators) |
| ValidatorFn | [https://angular.cn/api/forms/ValidatorFn#validatorfn](https://angular.cn/api/forms/ValidatorFn#validatorfn) |
| ValidationErrors | [https://angular.cn/api/forms/ValidationErrors](https://angular.cn/api/forms/ValidationErrors) |
| 本節(jié)源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.2.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.2.2.zip) |
- 序言
- 第一章 Hello World
- 1.1 環(huán)境安裝
- 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 請求后臺(tái)數(shù)據(jù)
- 2.2.1 HttpClient
- 2.2.2 請求數(shù)據(jù)
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調(diào)函數(shù)
- 2.2.5 集成測試
- 2.2.6 本章小節(jié)
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對接后臺(tái)
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數(shù)
- 2.4.3 插值與模板表達(dá)式
- 2.4.4 初識(shí)泛型
- 2.4.5 更新教師
- 2.4.6 測試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標(biāo)庫
- 2.6.3 firefox
- 2.7 總結(jié)
- 第三章 用戶登錄
- 3.1 初識(shí)單元測試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結(jié)
- 第四章 個(gè)人中心
- 4.1 原型
- 4.2 管道
- 4.3 對接后臺(tái)
- 4.4 x-auth-token認(rèn)證
- 4.5 攔截器
- 4.6 小結(jié)
- 第五章 系統(tǒng)菜單
- 5.1 延遲及測試
- 5.2 手動(dòng)創(chuàng)建組件
- 5.3 隱藏測試信息
- 5.4 規(guī)劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結(jié)
- 第六章 班級管理
- 6.1 新增班級
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級
- 6.1.3 ApiInterceptor
- 6.1.4 數(shù)據(jù)驗(yàn)證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構(gòu)
- 6.1.8 小結(jié)
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應(yīng)式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應(yīng)用組件
- 6.2.5 小結(jié)
- 6.3 班級列表
- 6.3.1 原型設(shè)計(jì)
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態(tài)分頁
- 6.3.5 動(dòng)態(tài)分頁
- 6.3.6 @Input()
- 6.4 編輯班級
- 6.4.1 測試模塊
- 6.4.2 響應(yīng)式表單驗(yàn)證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構(gòu)
- 6.4.7 小結(jié)
- 6.5 刪除班級
- 6.6 集成測試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉(zhuǎn)
- 6.6.4 ngStyle
- 6.7 初識(shí)Service
- 6.7.1 catchError
- 6.7.2 單例服務(wù)
- 6.7.3 單元測試
- 6.8 小結(jié)
- 第七章 學(xué)生管理
- 7.1 班級列表組件
- 7.2 新增學(xué)生
- 7.2.1 exports
- 7.2.2 自定義驗(yàn)證器
- 7.2.3 異步驗(yàn)證器
- 7.2.4 再識(shí)DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結(jié)
- 7.3 單元測試進(jìn)階
- 7.4 學(xué)生列表
- 7.4.1 JSON對象與對象
- 7.4.2 單元測試
- 7.4.3 分頁模塊
- 7.4.4 子組件測試
- 7.4.5 重構(gòu)分頁
- 7.5 刪除學(xué)生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向?qū)ο?/a>
- 7.6 集成測試
- 7.7 編輯學(xué)生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學(xué)生
- 7.7.4 集成測試
- 7.7.5 可訂閱的路由參數(shù)
- 7.7.6 小結(jié)
- 7.8 總結(jié)
- 第八章 其它
- 8.1 打包構(gòu)建
- 8.2 發(fā)布部署
- 第九章 總結(jié)