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 FormsModule
與 ReactiveFormsModule
:
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; } constructor (private formBuilder: FormBuilder ) {} 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 { } 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 的概念是將表單 用程式 的方式產生。以這個需求來說,這個表單底下會有兩個欄位 account
與 password
,如果將其用 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 的類型即可
關於錯誤訊息基本上可以沿用上一篇的程式,只不過原本是傳入 FormControl
的 errors
來判斷,但現在是傳入整個 FormControl
,為什麼呢?
因為如果只有傳入 FormControl
的 errors
的話,你會發現表單初始化完之後,就會有錯誤訊息顯示在畫面上:
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 的朋友們,今天的資訊量會比較多,但重點大致上可歸納成以下四點:
學習如何將表單 用程式 的方式寫出來,心法:「資料即表單,表單即資料」 。
學習如何使用表單物件 FormBuilder 、 FormGroup 與 FormControl 。
學習如何使用 Validators 來驗證使用者所輸入的值。
學習如何將表單物件與 Template 綁定。
此外,千萬記得要 import FormsModule
與 ReactiveFormsModule
才可以使用噢!
我一樣會將今日的實作程式碼放在 Stackblitz 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果有任何的問題或是回饋,也都非常歡迎留言給我讓我知道噢!