iT邦幫忙

2021 iThome 鐵人賽

DAY 2
0
Modern Web

Angular 深入淺出三十天:表單與測試系列 第 2

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

Day2

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

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

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

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

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

實作開始

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

<form>
  <p>
    <label for="account">帳號:</label>
    <input type="email" name="account" id="account">
  </p>
  <p>
    <label for="password">密碼:</label>
    <input type="password" name="password" id="password">
  </p>
  <p>
    <button type="submit">登入</button>
  </p>
</form>

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

Template view

接著到 app.module.ts 裡 import FormsModule

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

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

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

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

export class AppComponent {
  
  // 綁定在帳號欄位上
  account = '';

  // 綁定在密碼欄位上
  password = '';
  
  // 帳號欄位的錯誤訊息
  accountErrorMessage = '';
  
  // 密碼欄位的錯誤訊息
  passwordErrorMessage = '';

  /**
   * 綁定在帳號欄位上,當使用者改變登入帳號時,會觸發此函式,並取得對應的錯誤訊息
   *
   * @param {string} account
   * @param {ValidationErrors} errors
   */
  accountValueChange(account: string, errors: ValidationErrors): void {
    this.account = account;
    this.validationCheck(errors, 'account');
  }


  /**
   * 綁定在密碼欄位上,當使用者改變密碼時會觸發此函式
   * 
   * @param {string} password
   * @param {ValidationErrors} errors
   */
  passwordValueChange(password: string, errors: ValidationErrors): void {
    this.password = password;
    this.validationCheck(errors, 'password');
  }

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

  /**
   * 透過欄位裡的 ValidationErrors 來設定該欄位的錯誤訊息
   * 
   * @param {ValidationErrors} errors 欲驗證的欄位的錯誤 (by Angular)
   * @param {'account' | 'password'} fieldName 欄位名稱
   */
  private validationCheck(
    errors: ValidationErrors,
    fieldName: 'account' | 'password'
  ): void {
    let errorMessage: string;
    if (!errors) {
      errorMessage = '';
    } else if (errors.required) {
      errorMessage = '此欄位必填';
    } else if (errors.pattern) {
      errorMessage = '格式有誤,請重新輸入';
    } else if (errors.minlength) {
      errorMessage = '密碼長度最短不得低於8碼';
    }
    this.setErrorMessage(fieldName, errorMessage);
  }

  /**
   * 設定指定欄位的錯誤訊息
   * 
   * @param {'account' | 'password'} fieldName 欲設定錯誤訊息的欄位名稱
   * @param {string} errorMessage 欲設定的錯誤訊息
   */
  private setErrorMessage(
    fieldName: 'account' | 'password',
    errorMessage: string
  ): void {
    if (fieldName === 'account') {
      this.accountErrorMessage = errorMessage;
    } else {
      this.passwordErrorMessage = errorMessage;
    }
  }

}

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

<form #form="ngForm" (ngSubmit)="login()">
  <p>
    <label for="account">帳號:</label>
    <input
      type="email"
      name="account"
      id="account"
      required
      pattern="\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b"
      #accountNgModel="ngModel"
      [ngModel]="account"
      (ngModelChange)="accountValueChange(accountNgModel.value, accountNgModel.errors)"
    />
    <span class="error-message">{{ accountErrorMessage }}</span>
  </p>
  <p>
    <label for="password">密碼:</label>
    <input
      type="password"
      name="password"
      id="password"
      required
      #passwordNgModel="ngModel"
      [minlength]="8"
      [maxlength]="16"
      [ngModel]="password"
      (ngModelChange)="passwordValueChange(passwordNgModel.value, passwordNgModel.errors)"
    />
    <span class="error-message">{{ passwordErrorMessage }}</span>
  </p>
  <p>
    <button type="submit" [disabled]="form.invalid">登入</button>
  </p>
</form>

到目前為止的程式碼你看懂了多少呢?容我稍微說明一下:

  1. 首先是關於必填檢核,只要 <input ...> 欄位裡加上 HTML 原生的屬性 ─ required 即可。

  2. 帳號欄位的格式檢查則是使用原生的屬性 ─ pattern ,這個屬性可以直接使用正規表示法的方式來檢查使用者所輸入的值是否符合我們所訂定的格式。不過要注意的是,頭尾不需要特別加上 /^$/ ,所以整串表示法只需要用到中間的部份 ─ \b[\w\.-]+@[\w\.-]+\.\w{2,4}\b

對這個屬性較不熟悉的朋友可以參照 MDN 的說明文件

  1. 字數長度的檢核也是使用原生的屬性 ─ minlengthmaxlength 。這部份有兩個地方需要特別留意:

    1. 字數長度的檢核不會管你的字元是半型還是全型、是英文還是中文,每個字元都是一樣以一個長度來計算,如果有特別需求就不能使用這個方式處理。
    2. HTML 的原生屬性 ─ maxlength 是會阻擋使用者輸入的,當需求是要檢核長度但不能阻擋使用者輸入的話,就不能使用這個方式。
  2. 很多人剛學會用 Angular 的朋友,在使用 ngModel 時都會忘記這兩件事情:

    1. 引入 FormsModule
    2. input 要有 name 屬性
  3. 使用範本語法 #accountNgModel="ngModel"#passwordNgModel="ngModel" 來取得型別為 NgModel 的物件,因為我們可以從中取得該欄位的 valueerrors ,前者指定給其相關屬性,後者用以判斷該欄位的錯誤,以設定相對應的錯誤訊息。

單純使用 #accountNgModel#accountNgModel="ngModel" 的差別在於前者取得的是單純的 HTMLInputElement 物件。

  1. 使用範本語法 #form="ngForm" 來取得型別為 NgForm 的表單物件。

單純使用 #form#form="ngForm" 的差別在於前者取得的是單純的 HTMLFormElement 物件。

  1. 最後,則是將登入按鈕加上 [disabled]="form.invalid" 的綁定,讓按鈕在表單無效時,無法按下登入按鈕。

至此,我們就完成今天的目標囉!是不是很簡單呢?!

最後的結果應該要像這樣:

complete gif

本日小結

剛開始學習 Angular 的朋友,通常都會因為不熟悉 Angular 的語法而導致明明很簡單的功能卻要弄得很複雜。

今天的學習重點主要有以下三點:

  1. 學習如何使用 Angular 的範本語法取得 Angular 已經包裝好的物件,例如 #accountNgModel="ngModel"#form="ngForm"
  2. 學習使用表單物件 NgModelNgForm
  3. 善用 NgModel 裡的 ValidationErrors 取得相應的錯誤訊息。

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

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

錯誤回報更新

  • 2021-09-19 22:54:50 ,感謝熱心讀者「程凱大」指正錯誤,已移除所有的 FormControl ,原因是因為在 Template Driven Forms 的範圍裡, NgModel 本身已有我們所需之屬性,是我自己豬頭捨近求遠,再次衷心感謝。

上一篇
Angular 深入淺出三十天:表單與測試 Day01 - 前言
下一篇
Angular 深入淺出三十天:表單與測試 Day03 - Reactive Forms 實作 - 以登入為例
系列文
Angular 深入淺出三十天:表單與測試30

1 則留言

1
TD
iT邦新手 5 級 ‧ 2021-09-18 10:48:46

平常都是用 Reactive Forms,在這裡看到 Template Driven Forms 的實作,學到新東西了!

Leo iT邦新手 3 級 ‧ 2021-09-18 11:18:25 檢舉

Hi TD

很高興有幫到你,其實我個人平常也很少在用,但是對於很多初學者來說,我覺得這是一個必經之路。

我要留言

立即登入留言