Angular 深入淺出三十天:表單與測試 Day03 - Reactive Forms 實作 - 以登入為例

Day3

今天要來用 Reactive Forms 的方式實作一個簡單的登入系統,撇開 UI 不談,具體的功能需求規格跟昨天差不多,如下所示:

  • 帳號
    • 格式為 Email Address,相關規則請參考維基百科,此處則直接使用正規表示法 /^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi 來檢驗,驗證有誤時需在欄位後方顯示錯誤訊息:格式有誤,請重新輸入
    • 此欄位必填,驗證有誤時需需在欄位後方顯示錯誤訊息:此欄位必填
  • 密碼
    • 長度最短不得低於 8 碼,驗證有誤時需需在欄位後方顯示錯誤訊息:密碼長度最短不得低於8碼
    • 長度最長不得超過 16碼,驗證有誤時需需在欄位後方顯示錯誤訊息:密碼長度最長不得超過16碼
    • 此欄位必填,驗證有誤時需需在欄位後方顯示錯誤訊息:此欄位必填
  • 以上驗證皆需在使用者輸入時動態檢查
  • 任一驗證有誤時,登入按鈕皆呈現不可被點選之狀態。

規格需求看清楚之後,我們就來開始實作吧!

實作時大家可以自己開一個專案來練習,抑或是用 Stackblitz 開一個 Angular 的專案來練習,我就不再贅述囉!

如果正在閱讀此篇文章的你還不知道要怎麼開始一個 Angular 專案的話,請先閱讀我的 Angular 深入淺出三十天後再來閱讀此系列文章會比較恰當噢!

實作開始

首先我們先準備好基本的 HTML :

1
2
3
4
5
6
7
8
9
10
11
12
13
<form>
<p>
<label for="account">帳號:</label>
<input type="email" id="account">
</p>
<p>
<label for="password">密碼:</label>
<input type="password" id="password">
</p>
<p>
<button type="submit">登入</button>
</p>
</form>

未經美化的畫面應該會長這樣:

Template view

接著到 app.module.ts 裡 import FormsModuleReactiveFormsModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }

然後將要綁在 Template 的屬性跟方法都準備好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
export class LoginComponent implements OnInit {

// 綁定在表單上
formGroup: FormGroup;

/**
* 用以取得帳號欄位的表單控制項
*/
get accountControl(): FormControl {
return this.formGroup.get('account') as FormControl;
}

/**
* 用以取得密碼欄位的表單控制項
*/
get passwordControl(): FormControl {
return this.formGroup.get('password') as FormControl;
}

/**
* 透過 DI 取得 FromBuilder 物件,用以建立表單
*/
constructor(private formBuilder: FormBuilder) {}

/**
* 當 Component 初始化的時候初始化表單
*/
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
account: [
'',
[
Validators.required,
Validators.pattern(/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi)
]
],
password: [
'',
[
Validators.required,
Validators.minLength(8),
Validators.maxLength(16)
]
]
});
}

// 綁定在表單上,當使用者按下登入按鈕時會觸發此函式
login(): void {
// do login...
}

/**
* 透過該欄位的表單控制項來取得該欄位的錯誤訊息
*
* @param {FormControl} formControl 欲取得錯誤訊息的欄位的表單控制項 (by Angular)
*/
getErrorMessage(formControl: FormControl): string {
let errorMessage: string;
if (!formControl.errors || formControl.pristine) {
errorMessage = '';
} else if (formControl.errors.required) {
errorMessage = '此欄位必填';
} else if (formControl.errors.pattern) {
errorMessage = '格式有誤,請重新輸入';
} else if (formControl.errors.minlength) {
errorMessage = '密碼長度最短不得低於8碼';
} else if (formControl.errors.maxlength) {
errorMessage = '密碼長度最長不得超過16碼';
}
return errorMessage;
}

}

就可以將這些屬性和方法跟 Template 綁定在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<form [formGroup]="formGroup" (ngSubmit)="login()">
<p>
<label for="account">帳號:</label>
<input
type="email"
id="account"
[formControl]="accountControl"
/>
<span class="error-message">{{ getErrorMessage(accountControl) }}</span>
</p>
<p>
<label for="password">密碼:</label>
<input
type="password"
id="password"
[formControl]="passwordControl"
/>
<span class="error-message">{{ getErrorMessage(passwordControl) }}</span>
</p>
<p>
<button type="submit" [disabled]="formGroup.invalid">登入</button>
</p>
</form>

到目前為止的程式碼你看懂了多少呢?對於剛接觸 Angular 的表單的朋友來說,今天的資訊量可能會比較大,容我稍微說明一下:

Reactive Forms 的概念是將表單程式的方式產生。以這個需求來說,這個表單底下會有兩個欄位 accountpassword ,如果將其用 JSON 來表示的話,應該會長這樣:

1
2
3
4
{
"account": "",
"password": ""
}

從資料面來看, {} 代表表單, "account": """password": "" 則是裡面的兩個欄位。

而再將其轉換成 Reactive Forms 的概念的話, {} 代表的是 FormGroup"account": """password": "" 則代表的是 FormControl

所以在程式碼中我們可以看到我們宣告 formGroup: FromGroup; 並且在 template 中將其綁定在表單上:

1
2
3
<form [formGroup]="formGroup">
<!-- ... -->
</form>

並且把表單控制項綁定在對應的 input 欄位上:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 帳號欄位 -->
<input
type="email"
id="account"
[formControl]="accountControl"
/>

<!-- 密碼欄位 -->
<input
type="password"
id="password"
[formControl]="passwordControl"
/>

然後在 ngOnInit 裡透過 FormBuilder 來初始化表單:

1
2
3
4
5
6
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
account: '我是該欄位的初始值',
password: '我是該欄位的初始值'
});
}

如此一來,就可以在初始化過後,跟我們的 template 正確綁定了。

而如果當該欄位需要驗證時,就要在初始化時將格式調整成:

1
2
3
4
5
6
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
account: ['我是該欄位的初始值', /* 驗證器的擺放位置 */],
password: ['我是該欄位的初始值', /* 驗證器的擺放位置 */],
});
}

如果只有一個要驗證的項目則可以直接放入:

1
2
3
4
5
6
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
account: ['我是該欄位的初始值', Validators.required],
password: ['我是該欄位的初始值', Validators.required],
});
}

如果有多個要驗證的項目,就用 [] 將多個驗證項包起來再放入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
account: [
'我是該欄位的初始值',
[
Validators.required,
Validators.pattern(/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi)
]
],
password: [
'我是該欄位的初始值',
[
Validators.required,
Validators.minLength(8),
Validators.maxLength(16)
]
],
});
}

在這裡我們可以發現,上一篇使用 Template Driven Forms 實作時,是用 HTML 原生的屬性來驗證,而今天使用 Reactive Forms 實作時,則是用程式來驗證,如此一來,可以降低表單與 template 之間的依賴性,使得其更易於維護、重用與測試。

Validators 是 Angular 幫我們製作的驗證器,裡面有很多常用驗證器,詳細請參考官方文件

當然我們也可以自己客製驗證器,只要符合 ValidatorFn 的類型即可

關於錯誤訊息基本上可以沿用上一篇的程式,只不過原本是傳入 FormControlerrors 來判斷,但現在是傳入整個 FormControl ,為什麼呢?

因為如果只有傳入 FormControlerrors 的話,你會發現表單初始化完之後,就會有錯誤訊息顯示在畫面上:

img

這是因為當我們的表單初始化完之後,驗證器就會開始運作,所以的確那個兩個欄位是有那個錯誤沒錯,但其實這不是我們想要的行為,因為使用者根本就還沒有開始填表單,我們想要的是當使用者開始填表單之後,才會顯示對應的錯誤訊息,所以我們改傳入整個 FormControl ,它其中有幾個很好用的屬性可以使用:

  • pristine ─ 如果此屬性為 true ,代表該欄位是乾淨,沒有被輸入過值;反之則代表有被輸入過值,與 dirty 成反比。
  • touched ─ 如果此屬性為 true,代表該欄位曾經被碰(該欄位曾經被使用滑鼠 focus 過);反之則代表該欄位完全沒被碰過。
  • dirty ─ 如果此屬性為 true ,代表該欄位曾經被輸入過值,已經髒掉了;反之則代表該欄位是乾淨,沒有被輸入過值,與 pristine 成反比。

想知道更多可以參考官方文件: FormControl 與其抽象類別 AbstractControl

所以我們只要加上當該欄位是乾淨的,就不回傳錯誤訊息的判斷就可以了,像是這樣:

1
2
3
4
5
6
7
getErrorMessage(formControl: FormControl): string {
let errorMessage: string;
if (!formControl.errors || formControl.pristine) {
errorMessage = '';
}
// 其他省略...
}

最終結果:

complete gif

本日小結

對於第一次接觸 Reactive Forms 的朋友們,今天的資訊量會比較多,但重點大致上可歸納成以下四點:

  1. 學習如何將表單程式的方式寫出來,心法:「資料即表單,表單即資料」
  2. 學習如何使用表單物件 FormBuilderFormGroupFormControl
  3. 學習如何使用 Validators 來驗證使用者所輸入的值。
  4. 學習如何將表單物件與 Template 綁定。

此外,千萬記得要 import FormsModuleReactiveFormsModule 才可以使用噢!

我一樣會將今日的實作程式碼放在 Stackblitz 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!

如果有任何的問題或是回饋,也都非常歡迎留言給我讓我知道噢!

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×