通過驗證用戶輸入的準確性和完整性,可以提高整體的數(shù)據(jù)質(zhì)量。該頁面顯示了如何從 UI 驗證用戶輸入,以及如何在響應(yīng)式表單和模板驅(qū)動表單中顯示有用的驗證消息。
要獲取這里用講解表單驗證的響應(yīng)式表單和模板驅(qū)動表單的完整范例代碼。請運行現(xiàn)場演練 / 下載范例。
為了往模板驅(qū)動表單中添加驗證機制,你要添加一些驗證屬性,就像原生的 HTML 表單驗證器一樣。 Angular 會用指令來匹配這些具有驗證功能的指令。
每當表單控件中的值發(fā)生變化時,Angular 就會進行驗證,并生成一個驗證錯誤的列表(對應(yīng)著 ?INVALID
?狀態(tài))或者 null(對應(yīng)著 VALID 狀態(tài))。
你可以通過把 ?ngModel
?導(dǎo)出成局部模板變量來查看該控件的狀態(tài)。 比如下面這個例子就把 ?NgModel
?導(dǎo)出成了一個名叫 ?name
?的變量:
<input type="text" id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel">
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert">
<div *ngIf="name.errors?.['required']">
Name is required.
</div>
<div *ngIf="name.errors?.['minlength']">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors?.['forbiddenName']">
Name cannot be Bob.
</div>
</div>
注意這個例子講解的如下特性。
<input>
? 元素帶有一些 HTML 驗證屬性:?required
?和 ?minlength
?。它還帶有一個自定義的驗證器指令 ?forbiddenName
?。#name="ngModel"
? 把 ?NgModel
?導(dǎo)出成了一個名叫 ?name
?的局部變量。?NgModel
?把自己控制的 ?FormControl
?實例的屬性映射出去,讓你能在模板中檢查控件的狀態(tài),比如 ?valid
?和 ?dirty
?。<div>
? 元素的 ?*ngIf
? 展示了一組嵌套的消息 ?div
?,但是只在有“name”錯誤和控制器為 ?dirty
?或者 ?touched
?時才出現(xiàn)。<div>
? 為其中一個可能出現(xiàn)的驗證錯誤顯示一條自定義消息。比如 ?required
?、?minlength
?和 ?forbiddenName
?。為防止驗證程序在用戶有機會編輯表單之前就顯示錯誤,你應(yīng)該檢查控件的 ?dirty
?狀態(tài)或 ?touched
?狀態(tài)。
- 當用戶在被監(jiān)視的字段中修改該值時,控件就會被標記為 ?
dirty
?(臟)- 當用戶的表單控件失去焦點時,該控件就會被標記為 ?
touched
?(已接觸)
在響應(yīng)式表單中,事實之源是其組件類。不應(yīng)該通過模板上的屬性來添加驗證器,而應(yīng)該在組件類中直接把驗證器函數(shù)添加到表單控件模型上(?FormControl
?)。然后,一旦控件發(fā)生了變化,Angular 就會調(diào)用這些函數(shù)。
驗證器函數(shù)可以是同步函數(shù),也可以是異步函數(shù)。
驗證器類型 |
詳細信息 |
---|---|
同步驗證器 |
這些同步函數(shù)接受一個控件實例,然后返回一組驗證錯誤或 |
異步驗證器 |
這些異步函數(shù)接受一個控件實例并返回一個 Promise 或 Observable,它稍后會發(fā)出一組驗證錯誤或 |
出于性能方面的考慮,只有在所有同步驗證器都通過之后,Angular 才會運行異步驗證器。當每一個異步驗證器都執(zhí)行完之后,才會設(shè)置這些驗證錯誤。
你可以選擇編寫自己的驗證器函數(shù),也可以使用 Angular 的一些內(nèi)置驗證器。
在模板驅(qū)動表單中用作屬性的那些內(nèi)置驗證器,比如 ?required
?和 ?minlength
?,也都可以作為 ?Validators
?類中的函數(shù)使用。
要想把這個英雄表單改造成一個響應(yīng)式表單,還是要用那些內(nèi)置驗證器,但這次改為用它們的函數(shù)形態(tài)。參閱下面的例子。
ngOnInit(): void {
this.heroForm = new FormGroup({
name: new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
alterEgo: new FormControl(this.hero.alterEgo),
power: new FormControl(this.hero.power, Validators.required)
});
}
get name() { return this.heroForm.get('name'); }
get power() { return this.heroForm.get('power'); }
在這個例子中,?name
?控件設(shè)置了兩個內(nèi)置驗證器 - ?Validators.required
? 和 ?Validators.minLength(4)
? 以及一個自定義驗證器 ?forbiddenNameValidator
?。
所有這些驗證器都是同步的,所以它們作為第二個參數(shù)傳遞。注意,你可以通過把這些函數(shù)放到一個數(shù)組中傳入來支持多個驗證器。
這個例子還添加了一些 getter 方法。在響應(yīng)式表單中,你通常會通過它所屬的控件組(FormGroup)的 ?get
?方法來訪問表單控件,但有時候為模板定義一些 getter 作為簡短形式。
如果你到模板中找到 ?name
?輸入框,就會發(fā)現(xiàn)它和模板驅(qū)動的例子很相似。
<input type="text" id="name" class="form-control"
formControlName="name" required>
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert alert-danger">
<div *ngIf="name.errors?.['required']">
Name is required.
</div>
<div *ngIf="name.errors?.['minlength']">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors?.['forbiddenName']">
Name cannot be Bob.
</div>
</div>
這個表單與模板驅(qū)動的版本不同,它不再導(dǎo)出任何指令。相反,它使用組件類中定義的 ?name
?讀取器(getter)。
請注意,?required
?屬性仍然出現(xiàn)在模板中。雖然它對于驗證來說不是必須的,但為了無障礙性,還是應(yīng)該保留它。
內(nèi)置的驗證器并不是總能精確匹配應(yīng)用中的用例,因此有時你需要創(chuàng)建一個自定義驗證器。
考慮前面的響應(yīng)式式表單中的 ?forbiddenNameValidator
?函數(shù)。該函數(shù)的定義如下。
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {forbiddenName: {value: control.value}} : null;
};
}
這個函數(shù)是一個工廠,它接受一個用來檢測指定名字是否已被禁用的正則表達式,并返回一個驗證器函數(shù)。
在本例中,禁止的名字是“bob”; 驗證器會拒絕任何帶有“bob”的英雄名字。 在其它地方,只要配置的正則表達式可以匹配上,它可能拒絕“alice”或者任何其它名字。
?forbiddenNameValidator
?工廠函數(shù)返回配置好的驗證器函數(shù)。 該函數(shù)接受一個 Angular 控制器對象,并在控制器值有效時返回 null,或無效時返回驗證錯誤對象。 驗證錯誤對象通常有一個名為驗證秘鑰(?forbiddenName
?)的屬性。其值為一個任意詞典,你可以用來插入錯誤信息(?{name}
?)。
自定義異步驗證器和同步驗證器很像,只是它們必須返回一個稍后會輸出 null 或“驗證錯誤對象”的承諾(Promise)或可觀察對象,如果是可觀察對象,那么它必須在某個時間點被完成(complete),那時候這個表單就會使用它輸出的最后一個值作為驗證結(jié)果。(譯注:HTTP 服務(wù)是自動完成的,但是某些自定義的可觀察對象可能需要手動調(diào)用 complete 方法)
在響應(yīng)式表單中,通過直接把該函數(shù)傳給 ?FormControl
?來添加自定義驗證器。
this.heroForm = new FormGroup({
name: new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
alterEgo: new FormControl(this.hero.alterEgo),
power: new FormControl(this.hero.power, Validators.required)
});
在模板驅(qū)動表單中,要為模板添加一個指令,該指令包含了 validator 函數(shù)。比如,對應(yīng)的 ?ForbiddenValidatorDirective
?用作 ?forbiddenNameValidator
?的包裝器。
Angular 在驗證過程中會識別出該指令的作用,因為該指令把自己注冊成了 ?NG_VALIDATORS
?提供者,如下例所示。?NG_VALIDATORS
?是一個帶有可擴展驗證器集合的預(yù)定義提供者。
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
然后該指令類實現(xiàn)了 ?Validator
?接口,以便它能簡單的與 Angular 表單集成在一起。這個指令的其余部分有助于你理解它們是如何協(xié)作的:
@Directive({
selector: '[appForbiddenName]',
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
@Input('appForbiddenName') forbiddenName = '';
validate(control: AbstractControl): ValidationErrors | null {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
: null;
}
}
一旦 ?ForbiddenValidatorDirective
?寫好了,你只要把 ?forbiddenName
?選擇器添加到輸入框上就可以激活這個驗證器了。比如:
<input type="text" id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel">
注意,自定義驗證指令是用 ?useExisting
?而不是 ?useClass
?來實例化的。注冊的驗證程序必須是 ?ForbiddenValidatorDirective
?實例本身 - 表單中的實例,也就是表單中 ?forbiddenName
?屬性被綁定到了"bob"的那個。
如果用 ?useClass
?來代替 ?useExisting
?,就會注冊一個新的類實例,而它是沒有 ?forbiddenName
?的。
Angular 會自動把很多控件屬性作為 CSS 類映射到控件所在的元素上。你可以使用這些類來根據(jù)表單狀態(tài)給表單控件元素添加樣式。目前支持下列類:
.ng-valid
?.ng-invalid
?.ng-pending
?.ng-pristine
?.ng-dirty
?.ng-untouched
?.ng-touched
?.ng-submitted
? (只對 form 元素添加)
在下面的例子中,這個英雄表單使用 ?.ng-valid
? 和 ?.ng-invalid
? 來設(shè)置每個表單控件的邊框顏色。
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
.alert div {
background-color: #fed3d3;
color: #820000;
padding: 1rem;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: .5rem;
}
select {
width: 100%;
padding: .5rem;
}
跨字段交叉驗證器是一種自定義驗證器,可以對表單中不同字段的值進行比較,并針對它們的組合進行接受或拒絕。比如,你可能有一個提供互不兼容選項的表單,以便讓用戶選擇 A 或 B,而不能兩者都選。某些字段值也可能依賴于其它值;用戶可能只有當選擇了 A 之后才能選擇 B。
下列交叉驗證的例子說明了如何進行如下操作:
這些例子使用了交叉驗證,以確保英雄們不會通過填寫 Hero 表單來暴露自己的真實身份。驗證器會通過檢查英雄的名字和第二人格是否匹配來做到這一點。
該表單具有以下結(jié)構(gòu):
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
});
注意,?name
?和 ?alterEgo
?是兄弟控件。要想在單個自定義驗證器中計算這兩個控件,你就必須在它們共同的祖先控件中執(zhí)行驗證:?FormGroup
?。你可以在 ?FormGroup
?中查詢它的子控件,從而讓你能比較它們的值。
要想給 ?FormGroup
?添加驗證器,就要在創(chuàng)建時把一個新的驗證器傳給它的第二個參數(shù)。
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
}, { validators: identityRevealedValidator });
驗證器的代碼如下。
/** A hero's name can't match the hero's alter ego */
export const identityRevealedValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const name = control.get('name');
const alterEgo = control.get('alterEgo');
return name && alterEgo && name.value === alterEgo.value ? { identityRevealed: true } : null;
};
這個 ?identity
?驗證器實現(xiàn)了 ?ValidatorFn
?接口。它接收一個 Angular 表單控件對象作為參數(shù),當表單有效時,它返回一個 null,否則返回 ?ValidationErrors
?對象。
該驗證器通過調(diào)用 ?FormGroup
?的 ?get
?方法來檢索這些子控件,然后比較 ?name
?和 ?alterEgo
?控件的值。
如果值不匹配,則 hero 的身份保持秘密,兩者都有效,且 validator 返回 null。如果匹配,就說明英雄的身份已經(jīng)暴露了,驗證器必須通過返回一個錯誤對象來把這個表單標記為無效的。
為了提供更好的用戶體驗,當表單無效時,模板還會顯示一條恰當?shù)腻e誤信息。
<div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
Name cannot match alter ego.
</div>
如果 ?FormGroup
?中有一個由 ?identityRevealed
?驗證器返回的交叉驗證錯誤,?*ngIf
? 就會顯示錯誤,但只有當該用戶已經(jīng)與表單進行過交互的時候才顯示。
對于模板驅(qū)動表單,你必須創(chuàng)建一個指令來包裝驗證器函數(shù)。你可以使用?NG_VALIDATORS
?令牌來把該指令提供為驗證器,如下例所示。
@Directive({
selector: '[appIdentityRevealed]',
providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }]
})
export class IdentityRevealedValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
return identityRevealedValidator(control);
}
}
你必須把這個新指令添加到 HTML 模板中。由于驗證器必須注冊在表單的最高層,因此下列模板會把該指令放在 ?form
?標簽上。
<form #heroForm="ngForm" appIdentityRevealed>
為了提供更好的用戶體驗,當表單無效時,我們要顯示一個恰當?shù)腻e誤信息。
<div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert">
Name cannot match alter ego.
</div>
這在模板驅(qū)動表單和響應(yīng)式表單中都是一樣的。
異步驗證器實現(xiàn)了 ?AsyncValidatorFn
?和 ?AsyncValidator
?接口。它們與其同步版本非常相似,但有以下不同之處。
validate()
? 函數(shù)必須返回一個 Promise 或可觀察對象,
first
?、?last
?、?take
?或 ?takeUntil
?。異步驗證在同步驗證完成后才會發(fā)生,并且只有在同步驗證成功時才會執(zhí)行。如果更基本的驗證方法已經(jīng)發(fā)現(xiàn)了無效輸入,那么這種檢查順序就可以讓表單避免使用昂貴的異步驗證流程(比如 HTTP 請求)。
異步驗證開始之后,表單控件就會進入 ?pending
?狀態(tài)??梢詸z查控件的 ?pending
?屬性,并用它來給出對驗證中的視覺反饋。
一種常見的 UI 模式是在執(zhí)行異步驗證時顯示 Spinner(轉(zhuǎn)輪)。下面的例子展示了如何在模板驅(qū)動表單中實現(xiàn)這一點。
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner>
在下面的例子中,異步驗證器可以確保英雄們選擇了一個尚未采用的第二人格。新英雄不斷涌現(xiàn),老英雄也會離開,所以無法提前找到可用的人格列表。為了驗證潛在的第二人格條目,驗證器必須啟動一個異步操作來查詢包含所有在編英雄的中央數(shù)據(jù)庫。
下面的代碼創(chuàng)建了一個驗證器類 ?UniqueAlterEgoValidator
?,它實現(xiàn)了 ?AsyncValidator
?接口。
@Injectable({ providedIn: 'root' })
export class UniqueAlterEgoValidator implements AsyncValidator {
constructor(private heroesService: HeroesService) {}
validate(
control: AbstractControl
): Observable<ValidationErrors | null> {
return this.heroesService.isAlterEgoTaken(control.value).pipe(
map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
catchError(() => of(null))
);
}
}
構(gòu)造函數(shù)中注入了 ?HeroesService
?,它定義了如下接口。
interface HeroesService {
isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}
在真實的應(yīng)用中,?HeroesService
?會負責向英雄數(shù)據(jù)庫發(fā)起一個 HTTP 請求,以檢查該第二人格是否可用。 從該驗證器的視角看,此服務(wù)的具體實現(xiàn)無關(guān)緊要,所以這個例子僅僅針對 ?HeroesService
?接口來寫實現(xiàn)代碼。
當驗證開始的時候,?UniqueAlterEgoValidator
?把任務(wù)委托給 ?HeroesService
?的 ?isAlterEgoTaken()
? 方法,并傳入當前控件的值。這時候,該控件會被標記為 ?pending
? 狀態(tài),直到 ?validate()
? 方法所返回的可觀察對象完成(complete)了。
?isAlterEgoTaken()
? 方法會調(diào)度一個 HTTP 請求來檢查第二人格是否可用,并返回 ?Observable<boolean>
? 作為結(jié)果。?validate()
? 方法通過 ?map
?操作符來對響應(yīng)對象進行管道化處理,并把它轉(zhuǎn)換成驗證結(jié)果。
與任何驗證器一樣,如果表單有效,該方法返回 ?null
?,如果無效,則返回 ?ValidationErrors
?。這個驗證器使用 ?catchError
?操作符來處理任何潛在的錯誤。在這個例子中,驗證器將 ?isAlterEgoTaken()
? 錯誤視為成功的驗證,因為未能發(fā)出驗證請求并不一定意味著這個第二人格無效。你也可以用不同的方式處理這種錯誤,比如返回 ?ValidationError
?對象。
一段時間過后,這條可觀察對象鏈完成,異步驗證也就完成了。?pending
?標志位也設(shè)置為 ?false
?,該表單的有效性也已更新。
要以響應(yīng)式表單使用異步驗證器,請首先將驗證器注入組件類的構(gòu)造函數(shù)。
constructor(private alterEgoValidator: UniqueAlterEgoValidator) {}
然后,將驗證器函數(shù)直接傳遞給 ?FormControl
?以應(yīng)用它。
在以下示例中,?UniqueAlterEgoValidator
?的 ?validate
?函數(shù)將其傳遞給控件的 ?asyncValidators
?選項并將其綁定到注入到 ?HeroFormReactiveComponent
?中的 ?UniqueAlterEgoValidator
?實例,最終將其應(yīng)用于 ?alterEgoControl
?。?asyncValidators
?的值可以是單個異步驗證器函數(shù),也可以是函數(shù)數(shù)組。
const alterEgoControl = new FormControl('', {
asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)],
updateOn: 'blur'
});
要在模板驅(qū)動表單中使用異步驗證器,請創(chuàng)建一個新指令并在其上注冊 ?NG_ASYNC_VALIDATORS
?提供者。
在下面的示例中,該指令注入包含實際驗證邏輯的 ?UniqueAlterEgoValidator
?類,并在應(yīng)該進行驗證時由 Angular 觸發(fā)的 ?validate
?函數(shù)中調(diào)用它。
@Directive({
selector: '[appUniqueAlterEgo]',
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UniqueAlterEgoValidatorDirective),
multi: true
}
]
})
export class UniqueAlterEgoValidatorDirective implements AsyncValidator {
constructor(private validator: UniqueAlterEgoValidator) {}
validate(
control: AbstractControl
): Observable<ValidationErrors | null> {
return this.validator.validate(control);
}
}
然后,與使用同步驗證器一樣,將指令的選擇器添加到輸入以激活它。
<input type="text"
id="alterEgo"
name="alterEgo"
#alterEgo="ngModel"
[(ngModel)]="hero.alterEgo"
[ngModelOptions]="{ updateOn: 'blur' }"
appUniqueAlterEgo>
默認情況下,所有驗證程序在每次表單值更改后都會運行。對于同步驗證器,這通常不會對應(yīng)用性能產(chǎn)生明顯的影響。但是,異步驗證器通常會執(zhí)行某種 HTTP 請求來驗證控件。每次按鍵后調(diào)度一次 HTTP 請求都會給后端 API 帶來壓力,應(yīng)該盡可能避免。
你可以把 ?updateOn
?屬性從 ?change
?(默認值)改成 ?submit
?或 ?blur
?來推遲表單驗證的更新時機。
使用模板驅(qū)動表單時,可以在模板中設(shè)置該屬性。
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
使用響應(yīng)式表單時,可以在 ?FormControl
?實例中設(shè)置該屬性。
new FormControl('', {updateOn: 'blur'});
默認情況下,Angular 通過在 ?<form>
? 元素上添加 ?novalidate
?屬性來禁用原生 HTML 表單驗證,并使用指令將這些屬性與框架中的驗證器函數(shù)相匹配。如果你想將原生驗證與基于 Angular 的驗證結(jié)合使用,你可以使用 ?ngNativeValidate
?指令來重新啟用它。
更多建議: