本節我們為新增組件增加一些驗證。在正式編碼之前,為了使用組件與我們有個更好的交互,在單元測試的代碼中的最后一行啟動自動檢測變更:
```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();
});
});
```
## 加入驗證器
然后給名稱、學號、班級分別加一個`required`驗證器:
```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() {
```
測試一校驗正常。
## 自定義驗證器
一般情況下,只驗證是否為空是滿足不了實際的需求的,比如我們在這對手機號的驗證、郵箱的驗證。Angular支持自定義`FormControl`驗證器,來幫助我們實現Angular內置驗證器無法滿足的需求。比如當前需要一定驗證手機號是否正確的驗證器。
自定義驗證器與自定義`FormControl`有著相通之處:都需要符合某種規范。自定義`FormControl`使用了實現接口來達到符合規范的目的,自定義驗證器使用返回特定類型來達到符合規范的目的。
在自定義驗證器時,我們也像Angular一樣,把所有的驗證器放到一個類中。同時由于表單驗證器可供所有的項目使用,所以我們將其建立在項目的根目錄下,至于名字我們將其命名為`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 {
}
```
### 驗證手機號
然后我們在該方法中建立一個用于手機號驗證的靜態方法`phone`:
```typescript
import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
export class YzValidators {
/**
* 驗證手機號
*/
??static phone(control: AbstractControl①): ValidationErrors② | null {
return null; ③
}
}
```
- ??方法以`static`關鍵字標識
- 該方法將當前`FormControl`做為參數傳入,所以①處的類型必須是`AbstractControl`
- 該方法的返回值類型必須是`ValidationErrors | null`,該返回值類型決定了其是一個表單驗證器
- ③當驗證通過時,將返回nulll;驗證不通過時,將返回`ValidationErrors`類型的數據
> 實際上`phone()`方法是`ValidatorFn`的實現。
### 應用驗證器
自定義驗證器的用法與Angular內置驗證器的用法完全相同:
```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">
手機號格式不正確
</small>
</div>
```
此時由于驗證方法`phone()`的返回值為null,所以無論在手機號一欄中輸入什么內容,都不會報錯:

假設把返回值改成非null,則無論輸入的什么樣的值該驗證器都會報錯:
```typescript
static phone(control: AbstractControl): ValidationErrors | null {
return {phone: '手機號格式錯誤'};
}
```

## 調用時機
每當`FormControl`有數據變更時,對應的驗證器都會被執行一次:
```typescript
static phone(control: AbstractControl): ValidationErrors | null {
console.log('phone is called');
return {phone: '手機號格式錯誤'};
}
```
此時當我們變更表單中的手機號時,在控制臺中將反復打印相同的日志:

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

否則驗證失敗:

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

## 本節作業
嘗試完成用于驗證郵箱是否合法的郵箱驗證器,比如:
```typescript
static email(control: AbstractControl): ValidationErrors | null {
// 這里是邏輯實現
}
```
然后將此驗證器應用到郵箱字段上,并進行充分的測試。
| 名稱 | 鏈接 |
| ---------------- | ------------------------------------------------------------ |
| 定義自定義驗證器 | [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) |
| 本節源碼 | [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 環境安裝
- 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 發布部署
- 第九章 總結