iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 21
1
Modern Web

從零開始的點餐系統,Google好棒棒系列 第 21

[Day21] Angular 主要概念 - 表單功能

上一篇在實作 merchant service 時,有人應該注意到已經把新增、更新商家的方法寫出來了,現在就是要用 Angular 的內建表單把它們串起來,利用表單取得使用者提供的資料,之後更新商家清單的資訊。

Angular 表單系統

Template-driven forms

主要是透過 Template (component 的 html 檔) 中, element 加上 Directive 的方式去建立或更新 data ,較常用在比較簡單的表單,但彈性較低,本次範例就是以此表單來演練

Reactive forms

在 component 的 ts 檔實體化 form 的 model,相對來說更具擴展性、重用性與可預測性。

詳細的介紹可在參考官方文件

建立編輯商家資訊的跳窗

設置呼叫 Dialog 方法

這裡使用 Material 的 Dialog ,所以需要在 merchant-list.component.ts 注入 MatDialog, 並修改 openEditModal 的方法,他負責開啟 Dialog,在這次的範例裡他可能是新增商家資訊或編輯商家資訊。在第 23、29 行是開啟 Dialog 的方法,open 方法第 1 個參數是要參考的 Dialog component、第 2 個參數是給 Dialog 的組態物件。
第 35 行之後,是當 Dialog 關閉時會傳出型別為 Merchant 物件,要傳出什麼樣的 data 將會在 Dialog component 中設定。之後再依照開啟 Dailog 時的模式(新增或編輯),呼叫之前已經實作的 createMerchant 或 updateMerchant 方法。

如果版本在 Angular 9 之前,需要再 module 中把 Dialog component 加到 entryComponents 陣列中。參考這裡

// ...省略
export class MerchantListComponent implements OnInit {
  merchants$: Observable<Merchant[]>;

  constructor(
    private merchantsService: MerchantsService,
    public dialog: MatDialog
  ) {}

  ngOnInit(): void {
    this.merchants$ = this.merchantsService.getMerchants();
  }

  openEditModal(mode, merchantId?): void {
    let dialogRef;
    const modalComponent = MerchantEditComponent;
    const modalWith = '500px';
    if (merchantId) {
      this.merchantsService
        .getMerchantById(merchantId)
        .pipe(take(1))
        .subscribe((merchant) => {
          dialogRef = this.dialog.open(modalComponent, {
            width: modalWith,
            data: { mode, merchant },
          });
        });
    } else {
      dialogRef = this.dialog.open(modalComponent, {
        width: modalWith,
        data: { mode },
      });
    }

    dialogRef.afterClosed().subscribe((merchant) => {
      if (!merchant) return;
      switch (mode) {
        case 'create':
          this.createMerchant(merchant);
          break;
        case 'edit':
          this.updateMerchant(merchant);
          break;
      }
    });
  }
 // ...省略
}

修改 merchant-edit.component

在前面已經有建立 merchant-edit.component 了,我們現在將要做一些調整,目標如下

  • 取得傳入資料
  • 傳出使用者提供的資料

在 class 裡,需要 merchant 屬性(第 3 行),這將會是 Template-driven forms 要綁定的 model。然後注入 MAT_DIALOG_DATA,從此物件中取得傳來的 merchant(第 7 行),這裡要注意的是傳來的 merchant 是 by reference 的 object ,所以這裡需要做做深拷貝(第 13 行),避免在更動表單資訊時,同時改到外部 componet 的狀態。

 // ...省略
export class MerchantEditComponent implements OnInit {
  merchant = new Merchant();

  constructor(
    public dialogRef: MatDialogRef<MerchantEditComponent>,
    @Inject(MAT_DIALOG_DATA) public data,
    private clonerSevice: ClonerService
  ) {}

  ngOnInit(): void {
    if (this.data.merchant) {
      this.merchant = this.clonerSevice.deepClone(this.data.merchant);
    }
  }

  onNoClick(): void {
    this.dialogRef.close();
  }
}

Template-driven forms 的重點在於 element 的 Directives,我們把表單層級(ngForm)的模板指定給名為 merchantForm 的 Template reference variables(第 3 行),以及每個 field 層級 (ngModel) 的模板指定給對應名稱的 Template reference variables(第 12 行),這裡的用途在於可以在 template 中取用該物件,像是在顯示驗證訊息時(第 14 行)。每個 input 需定義 name (第 11 行) 且需要用 [(ngModel)] 綁定對應 model (剛剛在 class 裡的 merchant)(第 12 行)。

最後在關閉 Dailog 時,要利用[mat-dialog-close] directive 把使用者已經填好的資訊,且通過表單驗證後的資料傳出去(第 77 行)。

<h1 mat-dialog-title>店家資訊</h1>
<div mat-dialog-content>
  <form #merchantForm="ngForm">
    <div>
      <mat-form-field>
        <mat-label>店名</mat-label>
        <input
          matInput
          required
          [(ngModel)]="merchant.name"
          name="name"
          #name="ngModel"
        />
        <mat-error *ngIf="name.invalid">{{ "此欄位必填" }}</mat-error>
      </mat-form-field>
    </div>
    <div>
      <mat-form-field>
        <mat-label>地址</mat-label>
        <input
          matInput
          required
          [(ngModel)]="merchant.adress"
          name="adress"
          #adress="ngModel"
        />
        <mat-error *ngIf="adress.invalid">{{ "此欄位必填" }}</mat-error>
      </mat-form-field>
    </div>
    <div>
      <mat-form-field>
        <mat-label>電話</mat-label>
        <input
          matInput
          required
          [(ngModel)]="merchant.phone"
          name="phone"
          #phone="ngModel"
        />
        <mat-error *ngIf="phone.invalid">{{ "此欄位必填" }}</mat-error>
      </mat-form-field>
    </div>
    <div>
      <mat-form-field>
        <mat-label>網站</mat-label>
        <input
          matInput
          required
          [(ngModel)]="merchant.website"
          name="website"
          #website="ngModel"
        />
        <mat-error *ngIf="website.invalid">{{ "此欄位必填" }}</mat-error>
      </mat-form-field>
    </div>
    <div>
      <mat-form-field>
        <mat-label>圖片</mat-label>
        <input
          matInput
          required
          [(ngModel)]="merchant.logo"
          name="logo"
          #logo="ngModel"
        />
        <mat-error *ngIf="logo.invalid">{{ "此欄位必填" }}</mat-error>
      </mat-form-field>
    </div>
  </form>
</div>
<div mat-dialog-actions>
  <button mat-button (click)="onNoClick()">取消</button>
  <button
    [disabled]="!merchantForm.valid"
    mat-raised-button
    color="primary"
    [mat-dialog-close]="merchant"
  >
    儲存
  </button>
</div>

結語

因為本篇開發的表單邏輯並不複雜,所以使用 Template-driven forms 方式達到目的,可參考完整程式碼,下一篇將會介紹 Angular 的路由機制。


上一篇
[Day20] Angular 主要概念 - Services 與依賴注入
下一篇
[Day22] Angular 主要概念 - 路由
系列文
從零開始的點餐系統,Google好棒棒30

尚未有邦友留言

立即登入留言