iT邦幫忙

2026 iThome 鐵人賽

0
Build on Google AI

2026年,如何利用 Antigravity CLI、Gemini、各項技能及 MCP Server 建構基於 Firebase 的 Angular 應用系列 第 35 篇

Day 35: 使用 Email/Password 身分驗證保護 Angular Firebase AI 應用程式(第 2 部分:Signal Forms 登入互動視窗)

  • 分享至 

  • xImage
  •  

在本篇部落格文章中,我們將使用穩定版的 Angular Signal Forms API(@angular/forms/signals)建置登入互動視窗元件(SignInModalComponent),藉此處理電子郵件與密碼身分驗證。

包含電子郵件與密碼欄位的登入對話框
圖說:包含電子郵件與密碼欄位的登入對話框。


架構與核心概念

登入流程的運作方式如下:

  • Signal Form 表單驗證:我們建置一個包含必填 email 與 password 欄位的 Signal Form。當送出表單且驗證失敗時,onInvalid 會自動將焦點移至第一個無效的表單控制項。
  • Firebase 身分驗證:當使用者送出有效的表單欄位時,SignInModalComponent 會呼叫 AuthService.signIn(),驗證所送出的電子郵件與密碼是否符合 Firebase Authentication 中的現有帳號。
  • 路由重新導向或錯誤處理:
    • 有效憑證:當身分驗證成功(AuthService.isAuthenticated() 為 true)時,互動視窗會關閉並將使用者重新導向至 /dashboard 路由,讓使用者能在此執行 AI 任務。
    • 無效憑證/拋出錯誤:當身分驗證失敗時,SignInModalComponent.signIn() 會更新其 errorMessage Signal('Failed to sign in. Please check your credentials and try again.'),並回傳 { kind: 'serverError', message: 'Failed to submit form' }。

Flowchart of Authentication Flow


1. 定義 Signal Form 驗證綱要(Schema)

首先,我們從 @angular/forms/signals 匯入 required 與 email 驗證器來定義 signInSchemaValidation(),此函式會回傳 SchemaOrSchemaFn<AuthCredentials, PathKind.Root>:

  • 必填欄位(required):required 驗證器會將 email 與 password 兩個欄位皆標記為必填,並設定自訂錯誤訊息({ message: 'Email is required' } 與 { message: 'Password is required' })。
  • 電子郵件格式驗證(email):email 驗證器會檢查輸入的電子郵件值是否為格式正確的電子郵件地址,並在格式無效時回傳 { message: 'Invalid email format' }。
// src/app/shared/ui/sign-in-modal/schemas/sign-in.schema.ts
import type { AuthCredentials } from '@/core/auth';
import type { PathKind, SchemaOrSchemaFn } from '@angular/forms/signals';
import { email, required } from '@angular/forms/signals';

export function signInSchemaValidation(): SchemaOrSchemaFn<AuthCredentials, PathKind.Root> {
  return (schemaPath) => {
    required(schemaPath.email, { message: 'Email is required' });
    email(schemaPath.email, { message: 'Invalid email format' });
    required(schemaPath.password, { message: 'Password is required' });
  };
}

2. 使用 Signal Forms 與 AuthService 建置 SignInModalComponent

2.1 注入相依性與初始化 Signal Form

在 SignInModalComponent 中,我們注入所需的服務並初始化響應式表單模型:

  • AuthService 與 Router:負責處理登入後的 Firebase 電子郵件/密碼身分驗證與路由導覽。
  • DialogRef:當使用者點擊關閉按鈕或登入成功時,透過 @angular/cdk/dialog(inject(DialogRef, { optional: true }))關閉互動視窗。
  • errorMessage:當身分驗證失敗時,儲存並顯示警告訊息(signal<string | undefined>(undefined))。
  • signInModel:作為 Signal Form 的底層狀態模型(signal<AuthCredentials>({ email: '', password: '' })),並將 email 與 password 欄位初始化為空字串。

[!NOTE]
為什麼選擇 @angular/cdk/dialog 而非原生 <dialog> 元素?
如果您只需要在不依賴 @angular/cdk 的應用程式中實作單一互動視窗,原生的 HTML <dialog> 元素會是更佳的零相依性選擇。然而,此存放庫原本就已包含 @angular/cdk,且 @angular/cdk/dialog 內建完整的無障礙支援(a11y)——包含焦點鎖定(Focus Trapping)、焦點還原(Focus Restoration)、按 Escape 鍵關閉以及 ARIA 屬性。此外,由於我們在開啟互動視窗時也會動態匯入 @angular/cdk/dialog,因此它完全不會增加初始打包大小(Initial Bundle Size),在這裡是個相當務實的選擇。

// src/app/core/auth/auth-credentials.interface.ts
export interface AuthCredentials {
  email: string;
  password: string;
}
// src/app/shared/ui/sign-in-modal/sign-in-modal.component.ts (Part 1a: Dependencies & Model)
@Component({
  imports: [FormField, FormRoot, CloseIconComponent, SpinnerIconComponent],
  selector: 'app-sign-in-modal',
})
export class SignInModalComponent {
  readonly #authService = inject(AuthService);
  readonly #dialogRef = inject(DialogRef, { optional: true });
  readonly #router = inject(Router);

  errorMessage = signal<string | undefined>(undefined);

  signInModel = signal<AuthCredentials>({
    email: '',
    password: '',
  });

接著,我們將 this.signInModel 與 signInSchemaValidation() 傳遞給 form() 函式以建立 signInForm。form() 函式會根據驗證綱要來驗證 signInModel,並設定表單送出行為:

  • 有效送出(submission.action):當驗證成功時,表單送出會執行 action 回呼,進而呼叫 this.signIn(field)。
  • 無效送出(submission.onInvalid):當驗證失敗時,表單送出會執行 onInvalid 回呼以找出第一個驗證錯誤(field().errorSummary()[0]),並自動將焦點移至其繫結的表單控制項(firstError?.fieldTree().focusBoundControl())。
// src/app/shared/ui/sign-in-modal/sign-in-modal.component.ts (Part 1b: Signal Form)
  signInForm = form(this.signInModel, signInSchemaValidation(), {
    submission: {
      action: async (field) => this.signIn(field),
      onInvalid: (field) => {
        const firstError = field().errorSummary()[0];
        firstError?.fieldTree().focusBoundControl();
      },
    },
  });

2.2 實作 onClose() 與 signIn()

接下來,我們在 SignInModalComponent 中實作 onClose() 與 signIn() 方法:

  • onClose():當使用者點擊右上角的關閉按鈕時,呼叫 this.#dialogRef?.close() 以關閉互動視窗。
  • signIn():呼叫 this.#authService.signIn(field().value()),向 Firebase Authentication 驗證所送出的表單憑證:
    • 身分驗證成功:當 this.#authService.isAuthenticated() 的評估結果為 true 時,我們會關閉互動視窗(this.#dialogRef?.close())、導覽至 /dashboard 路由(await this.#router.navigate([APP_LINKS.DASHBOARD])),並回傳 undefined。
    • 身分驗證失敗:當身分驗證失敗時,我們會更新 this.errorMessage.set('Failed to sign in. Please check your credentials and try again.'),並回傳通用送出錯誤({ kind: 'serverError', message: 'Failed to submit form' }),讓 Signal Form 記錄送出失敗的狀態。
// src/app/shared/ui/sign-in-modal/sign-in-modal.component.ts (Part 2: onClose & signIn)
  onClose(): void {
    this.#dialogRef?.close();
  }

  async signIn(field: FieldTree<AuthCredentials, string | number>): Promise<SignInSubmissionError | undefined> {
    this.errorMessage.set(undefined);
    await this.#authService.signIn(field().value());
    if (this.#authService.isAuthenticated()) {
      this.#dialogRef?.close();
      await this.#router.navigate([APP_LINKS.DASHBOARD]);
      return undefined;
    }
    this.errorMessage.set('Failed to sign in. Please check your credentials and try again.');
    return { kind: 'serverError', message: 'Failed to submit form' };
  }
}

3. 在範本中繫結 Signal Form

現在,讓我們在 src/app/shared/ui/sign-in-modal/sign-in-modal.component.html 中將 signInForm 串接至元件範本:

  • 關閉互動視窗:當使用者點擊右上角的關閉按鈕時,onClose() 會透過 this.#dialogRef?.close() 關閉互動視窗。
  • 顯示身分驗證錯誤:當 errorMessage() 包含訊息(@if (errorMessage(); as msg))時,範本會在表單上方渲染 <div role="alert">{{ msg }}</div>。
  • 繫結表單根元素([formRoot]):[formRoot] 指令會將 <form> 元素直接繫結至 signInForm 表單模型,將表單送出事件串接至我們在元件中定義的 submission 設定。
  • 繫結表單欄位([formField]):[formField] 指令會將 signInForm.email 繫結至電子郵件輸入欄位,並將 signInForm.password 繫結至密碼輸入欄位。
  • 渲染驗證錯誤:當使用者與欄位互動過(touched())且該欄位無效(invalid())時,我們會使用 @for 走訪其 errors() 陣列,輸出每一則驗證錯誤訊息({{ error.message }})。
  • 處理送出狀態:當表單的非同步 submission.action 正在執行時,signInForm().submitting() 的評估結果會是 true。我們在送出按鈕上繫結 [disabled]="signInForm().submitting()",藉此在身分驗證期間停用按鈕並防止重複送出。
<!-- src/app/shared/ui/sign-in-modal/sign-in-modal.component.html (Signal Form excerpt) -->
@if (errorMessage(); as msg) {
  <div role="alert">{{ msg }}</div>
}

<form [formRoot]="signInForm">
  <label for="sign-in-email">
    EMAIL ADDRESS
    <input id="sign-in-email" type="email" autocomplete="username" [formField]="signInForm.email" />
    @if (signInForm.email().touched() && signInForm.email().invalid()) {
      @for (error of signInForm.email().errors(); track error) {
        <span>{{ error.message }}</span>
      }
    }
  </label>

  <label for="sign-in-password">
    PASSWORD
    <input id="sign-in-password" type="password" autocomplete="current-password" [formField]="signInForm.password" />
    @if (signInForm.password().touched() && signInForm.password().invalid()) {
      @for (error of signInForm.password().errors(); track error) {
        <span>{{ error.message }}</span>
      }
    }
  </label>

  <button type="submit" [disabled]="signInForm().submitting()">
    @if (signInForm().submitting()) {
      <app-spinner-icon />
      <span>Signing in...</span>
    } @else {
      <span>Sign In</span>
    }
  </button>
</form>

登入驗證失敗時的登入對話框
圖說:登入驗證失敗時的登入對話框。

表單欄位違反必填欄位驗證規則時的登入對話框
圖說:表單欄位違反必填欄位驗證規則時的登入對話框。


重點回顧與下一步

在第 2 部分中,我們建置了 SignInModalComponent 並將其與 AuthService 串接:

  • Angular Signal Forms 整合:SignInModalComponent 使用 Angular Signal Forms 定義表單模型,並將每個欄位繫結至對應的 HTML <input> 元素。signInSchemaValidation() 負責驗證表單欄位。當驗證通過時,submission.action 會呼叫 AuthService.signIn() 以驗證使用者的憑證;當驗證失敗時,submission.onInvalid 會自動將焦點移至第一個發生錯誤的欄位。
  • 透過動態匯入維持精簡的初始打包大小:儘管 @angular/cdk 並非輕量級函式庫,我們仍會在從 Home 元件開啟互動視窗時動態匯入 SignInModalComponent 與 @angular/cdk/dialog。這能確保 main.js 與初始打包檔案保持精簡。

在第 3 部分中,我們將建置一個輕量級的 Home 首頁畫面,其中包含可開啟 CDK Dialog 的 Sign In 按鈕。此外,我們也會更新 Header 元件以提供返回首頁導覽與登出的圖示。最後,我們將使用 AuthService.isAuthenticated() 計算型 Signal(Computed Signal),在首頁畫面上依條件顯示 Sign In 與 Launch Studio 按鈕,並在 Header 元件中重複使用同一個 Signal 來顯示或隱藏登出按鈕。

參考資源


上一篇
Day 34: 使用 Email/Password 身分驗證保護 Angular Firebase AI 應用程式(第 1 部分:Emulator、AuthService 與路由守衛)
系列文
2026年,如何利用 Antigravity CLI、Gemini、各項技能及 MCP Server 建構基於 Firebase 的 Angular 應用 共 35 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言