使用 Template-Driven Form 是將驗證的部分寫在 Template 中,而 Reactive Form 把驗證的地方寫在 TypeScript 中,來看看需要更改的檔案 :
|--app
|--app.component.html // 更動
|--app.component.ts // 更動
|--app.module.ts // 更動
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username">
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
formControlName="email">
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
[value]="gender"
formControlName="gender">{{ gender }}
</label>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
</div>
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit(): void {
this.signupForm = new FormGroup({
username: new FormControl(),
email: new FormControl(),
gender: new FormControl()
});
}
onSubmit() {
console.log(this.signupForm)
}
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
要在 AppModule
中 import ReactiveFormsModule
。TypeScript 的部分,class 內部宣告型別為 FormGroup
的物件並在 ngOnInit
中初始化它,FormGroup
建構子的參數是放入物件,物件的 key 會對應到 Template 的設定 ( <input>
中的 formControlName
),物件的 value 則是 FormControl
物件 ( 建構子可以寫入驗證的方式以及預設值,目前先留空及可 )。Template 的部分,在 <form>
上面綁定了 Property Binding 到 TypeScript 中的 FormGroup
物件,而內部想要驗證的 <input>
也綁定了 formControlName
,這就完成了 TypeScript 與 Template 的交互綁定。按下按鈕觸發 ngSubmit(..)
後直接操作 FormGroup
物件,此時在開發人員工具應該可以看到 FormGroup
的內容,以及可以發現各個 <input>
也具有前面 Template-Driven Form 所說的三組 class ( dirty
、touched
、invalid
)。
下步驟是增加驗證功能 :
|--app
|--app.component.html // 更動
|--app.component.ts // 更動
|--app.component.css // 更動
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username">
<span
*ngIf="signupForm.get('username').invalid && signupForm.get('username').touched"
class="help-block">Please enter a valid username!</span>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
formControlName="email">
<span
*ngIf="signupForm.get('email').invalid && signupForm.get('email').touched"
class="help-block">Please enter a valid email!</span>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
[value]="gender"
formControlName="gender">{{ gender }}
</label>
</div>
<span *ngIf="signupForm.invalid && signupForm.touched"
class="help-block">Please enter a valid data!</span>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
</div>
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit(): void {
this.signupForm = new FormGroup({
username: new FormControl(null, Validators.required),
email: new FormControl(null, [Validators.required, Validators.email]),
gender: new FormControl('female')
});
}
onSubmit() {
console.log(this.signupForm)
}
}
.container {
margin-top: 30px;
}
input.ng-invalid.ng-touched {
border: 1px solid red;
}
TypeScript 的部分,在 FormControl
中會有兩個參數,第一個參數是設定此 <input>
帶有的預設值,第二個參數可以傳入陣列或是單一的 method
,主要是放入驗證的 method
,先採用預設的 Validators...
的方法。在 Template 的部分,為了出現和先前一樣的效果,不使用範本參考變數而是改成 *ngIf
去取得放在 TypeScript 中的值,來取得驗證是否成功以及使用者是否有觸及此 <input>
,在 css 的部分,則是使用 Angular 設定的 class 給予驗證未通過的 <input>
加上紅色邊框。
|--app
|--app.component.html // 更動
|--app.component.ts // 更動
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div formGroupName="userdata">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username">
<span
*ngIf="signupForm.get('userdata.username').invalid && signupForm.get('userdata.username').touched"
class="help-block">Please enter a valid username!</span>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
formControlName="email">
<span
*ngIf="signupForm.get('userdata.email').invalid && signupForm.get('userdata.email').touched"
class="help-block">Please enter a valid email!</span>
</div>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
[value]="gender"
formControlName="gender">{{ gender }}
</label>
</div>
<span *ngIf="signupForm.invalid && signupForm.touched"
class="help-block">Please enter a valid data!</span>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
</div>
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit(): void {
this.signupForm = new FormGroup({
userdata: new FormGroup({
username: new FormControl(null, Validators.required),
email: new FormControl(null, [Validators.required, Validators.email])
}),
gender: new FormControl('female')
});
}
onSubmit() {
console.log(this.signupForm)
}
}
在 FormGroup
中可以包入另一個 FormGroup
,並且給予這個 FormGroup
另一個名稱,而 Template 必須在額外添加 <div>
,並給予它 formGroupName
來代表內部 FormGroup
的 key 值。
另一個是 FormArray
,如果有許多相同類型的 <input>
要做驗證,就會用到這個物件 :
|--app
|--app.component.html // 更動
|--app.component.ts // 更動
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div formGroupName="userdata">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username">
<span
*ngIf="signupForm.get('userdata.username').invalid && signupForm.get('userdata.username').touched"
class="help-block">Please enter a valid username!</span>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
formControlName="email">
<span
*ngIf="signupForm.get('userdata.email').invalid && signupForm.get('userdata.email').touched"
class="help-block">Please enter a valid email!</span>
</div>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
[value]="gender"
formControlName="gender">{{ gender }}
</label>
</div>
<div formArrayName="hobbies">
<h4>Your hobbies</h4>
<button type="button"
class="btn btn-default"
(click)="onAddHobby()">Add hobby</button>
<div class="form-group"
*ngFor="let hobbyControl of hobbies; let i = index">
<input type="text" class="form-control" [formControlName]="i">
</div>
</div>
<span *ngIf="signupForm.invalid && signupForm.touched"
class="help-block">Please enter a valid data!</span>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
</div>
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit(): void {
this.signupForm = new FormGroup({
userdata: new FormGroup({
username: new FormControl(null, Validators.required),
email: new FormControl(null, [Validators.required, Validators.email])
}),
gender: new FormControl('female'),
hobbies: new FormArray([])
});
}
onSubmit() {
console.log(this.signupForm)
}
onAddHobby() {
const control = new FormControl(null, Validators.required);
(this.signupForm.get('hobbies') as FormArray).push(control);
}
get hobbies() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
}
TypeScript 的部分,在最原本的 FormGroup
中新增一個 key 值並對應到 FormArray
物件,預期上會希望在前端按下按鈕後,新增一個 <input>
,因此在這邊新增 onAddHobby(..)
方法,而 get hobbies(..)
則是為了讓前端 Template 的部分可以使用 *ngFor
而開出的 get(..)
方法 ( 寫成 method 是為了取出 FromArray
內部的 controls
,也可以在直接寫在 Template 中,只是 Template 的內容看起來會較為複雜 )。Template 中則多出一整個 <div>
區塊,並給予 formArrayName
名稱對應到 TypeScript 的部分,最後使用 *ngFor
來把 TypeScript 中的 FormArray
給一一迭代出來。
除了預設的驗證外,還可使用自定義的驗證 :
|--app
|--app.component.html // 更動
|--app.component.ts // 更動
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div formGroupName="userdata">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username">
<span
*ngIf="signupForm.get('userdata.username').invalid && signupForm.get('userdata.username').touched"
class="help-block">
<span *ngIf="signupForm.get('userdata.username').errors['nameIsForbidden']">This name is invalid!</span>
<span *ngIf="signupForm.get('userdata.username').errors['required']">This field is required!</span>
</span>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
formControlName="email">
<span
*ngIf="signupForm.get('userdata.email').invalid && signupForm.get('userdata.email').touched"
class="help-block">Please enter a valid email!</span>
</div>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
[value]="gender"
formControlName="gender">{{ gender }}
</label>
</div>
<div formArrayName="hobbies">
<h4>Your hobbies</h4>
<button type="button"
class="btn btn-default"
(click)="onAddHobby()">Add hobby</button>
<div class="form-group"
*ngFor="let hobbyControl of hobbies; let i = index">
<input type="text" class="form-control" [formControlName]="i">
</div>
</div>
<span *ngIf="signupForm.invalid && signupForm.touched"
class="help-block">Please enter a valid data!</span>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
</div>
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
forbiddenUserNames = ['Chris', 'Anna'];
ngOnInit(): void {
this.signupForm = new FormGroup({
userdata: new FormGroup({
username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
email: new FormControl(null, [Validators.required, Validators.email])
}),
gender: new FormControl('female'),
hobbies: new FormArray([])
});
}
get hobbies() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
onSubmit() {
console.log(this.signupForm)
}
onAddHobby() {
const control = new FormControl(null, Validators.required);
(this.signupForm.get('hobbies') as FormArray).push(control);
}
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if(this.forbiddenUserNames.indexOf(control.value) !== -1){
return {
nameIsForbidden: true
};
}
return null;
}
}
在 TypeScript 中定義了一個新 method,參數上放入 FormControl
物件,代表想要進行驗證的物件,回傳則定義了一組物件,key 定義為 string
、value 定義為 boolean
,如果驗證失敗就回傳此物件,成功時則回傳 null
( 不是把 true
更改成 false
,這個物件是驗證失敗時傳遞給外部的訊息,null
才是驗證成功, )。接著在上方的 signupForm
補上自己定義好的方法,並且使用 bind(..)
明確綁定當下的 this 為何。在 Template 中則是新增幾個 <span>
來代表驗證失敗時應該要呈現的訊息。
有時驗證的資料並不會馬上取得,像 Promise 有三種狀態 fulfillment、rejeciton、pending,在 pending 狀態時表示資料還未取得,這時可以使用非同步的方式來進行驗證 ( 等資料來時才驗證 ) :
|--app
|--app.component.ts // 更動
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
forbiddenUserNames = ['Chris', 'Anna'];
ngOnInit(): void {
this.signupForm = new FormGroup({
userdata: new FormGroup({
username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
}),
gender: new FormControl('female'),
hobbies: new FormArray([])
});
}
get hobbies() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
onSubmit() {
console.log(this.signupForm)
}
onAddHobby() {
const control = new FormControl(null, Validators.required);
(this.signupForm.get('hobbies') as FormArray).push(control);
}
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if(this.forbiddenUserNames.indexOf(control.value) !== -1){
return {
nameIsForbidden: true
};
}
return null;
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if(control.value === '[email protected]'){
resolve({emailIsForbidden: true});
} else {
resolve(null);
}
}, 1500)
});
return promise;
}
}
這邊新增驗證 email 的方法,傳入參數定義為 FormControl
,回傳則定義為非同步物件 Promise
或是 Observable
,並且寫下方法內部的細節,這邊所採用的是 Promise
,並用 setTimeout(..)
來設定非同步訊息,無論驗證成功與否都會執行 resolve(..)
方法,只是驗證失敗時是在內部傳入物件 ( 成功是 null
,表示不用帶任何訊息 ),並在最後必須回傳 Promise
物件。如果打開開發人員工具,會發現當 <input>
內部有所變動時,原本的 ng-valid
、ng-invalid
會轉變為 ng-pending
一下子,原因就在於 setTimeout(..)
的非同步設定,讓回傳的 Promise 還在 pending 狀態。
可以使用 valueChanges(..)
以及 statusChanges(..)
這兩個方法進行監控,範例中是在 ngOnInit(..)
中進行訂閱 :
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
forbiddenUserNames = ['Chris', 'Anna'];
ngOnInit(): void {
this.signupForm = new FormGroup({
userdata: new FormGroup({
username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
}),
gender: new FormControl('female'),
hobbies: new FormArray([])
});
this.signupForm.valueChanges.subscribe(console.log);
this.signupForm.statusChanges.subscribe(console.log);
}
get hobbies() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
onSubmit() {
console.log(this.signupForm)
}
onAddHobby() {
const control = new FormControl(null, Validators.required);
(this.signupForm.get('hobbies') as FormArray).push(control);
}
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if(this.forbiddenUserNames.indexOf(control.value) !== -1){
return {
nameIsForbidden: true
};
}
return null;
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if(control.value === '[email protected]'){
resolve({emailIsForbidden: true});
} else {
resolve(null);
}
}, 1500)
});
return promise;
}
}
嘗試讓驗證狀態以及 <input>
內部的值改動,可以在開發人員工具看到訊息。
與 Template-Driven Form 相同,可以使用的這個三個方法去設定表單的內容以及值,唯一不同的只是操作的物件對象不一樣而已 :
export class AppComponent implements OnInit{
genders = ['male', 'female'];
signupForm: FormGroup;
forbiddenUserNames = ['Chris', 'Anna'];
ngOnInit(): void {
this.signupForm = new FormGroup({
userdata: new FormGroup({
username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
}),
gender: new FormControl('female'),
hobbies: new FormArray([])
});
this.signupForm.valueChanges.subscribe(console.log);
this.signupForm.statusChanges.subscribe(console.log);
this.signupForm.setValue({
userdata: {
username: 'Max',
email: '[email protected]'
},
gender: 'male',
hobbies: []
});
this.signupForm.patchValue({
userdata: {
username: 'Anna',
}
});
}
get hobbies() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}
onSubmit() {
console.log(this.signupForm)
this.signupForm.reset();
}
onAddHobby() {
const control = new FormControl(null, Validators.required);
(this.signupForm.get('hobbies') as FormArray).push(control);
}
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if(this.forbiddenUserNames.indexOf(control.value) !== -1){
return {
nameIsForbidden: true
};
}
return null;
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if(control.value === '[email protected]'){
resolve({emailIsForbidden: true});
} else {
resolve(null);
}
}, 1500)
});
return promise;
}
}
FormControl
、FormGroup
、FormArray
- 自定義驗證方法
- 自定義非同步驗證方法
setValue(..)
、patchValue(..)
、reset(..)