iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Modern Web

關於我作夢變成工程師這檔事(Angular 篇)系列 第 10

第 10 天 別說呂布了,你聽過青銅五小強嗎 |Template-driven-form、ngModel、Template variables

前情提要

昨日我們聊了一些關於「頁面」與「元件」在規劃上,可能需要注意的地方。今天,我們會實際帶著「頁面還是元件」這樣的問題意識,來實作新增英雄表單功能,在表單方面,我們將演示範本驅動的表單。

Angular 在表單方面,提供了「範本驅動」(Template driven form)及「響應式」(Reactive From)兩種不同的方式。簡要來說,前者多數程式會撰寫在 HTML 檔案、處理較簡單的表單內容,後者多數程式會撰寫在 TS 檔案,可以更好地處理邏輯較複雜的表單。

008

(你終於領悟了要怎麼召喚英雄了。不過,你後面揹一個大卷軸是在幹嘛?)

看來妳是不懂什麼叫做仙人模式啊,大閒人...話說妳消失了很多天?

(哼哼...上次因為經費不足讓英雄陣容縮編,讓我痛定思痛...)

妳真的去買狗狗幣了?

(不,我發現應該要尋找廉價勞工熱血的有志之士!)

聽著都不合法啊。

(你知道有一種人,不怎麼厲害,但是怎麼打也打不死。然後突然就小宇宙爆發了嗎?)

我好像知道妳在說什麼...

(而且就紀錄片來看,他們根本是弒神專門戶啊!)

呃,妳說的紀錄片是平面的還是 3D?

https://ithelp.ithome.com.tw/upload/images/20210925/20128395HNGfHpiup7.jpg
圖片來源:GreatGame

(特別是有個叫瞬的,只要招募到他,連他哥哥都會免費加入了,買一送一!而且他哥超厲害。)

妳再說下去,我都不知道自己在睡覺還是中了什麼幻魔拳了。

(總之我們快想辦法來讓他們簽下去吧!)

參考資料:不負責任瘋動漫。《【特別加映】聖鬥士星矢之五小強成長史「黃金12宮篇」》。

規劃新增英雄功能

如同昨日的討論,「新增英雄」功能會需要填寫「英雄資料表單」,而可以想見的是,日後大概率會提供「編輯英雄」功能,而它也應該是編輯一樣的「英雄資料表單」。但如果我們直接將「英雄資料表單」元件當作兩個功能路徑對應的頁面的話,那可能會需要額外處理邏輯,幅度隨兩個頁面在畫面上的異同程度增減。

一個比較好的做法應該是,建立兩個頁面層級的元件「新增英雄」頁面元件及「編輯英雄」頁面元件,並將「英雄資料表單」作為一個共用元件,目前的專案結構規劃應該是這樣:

src
⌞app
  ⌞ pages
      ⌞ AddHeroPageComponent
      ⌞ EditHeroPageComponent
  ⌞ shared
      ⌞ components
          HeroInfomationFormComponent

依序輸入下列指令:

ng g c pages/add-hero-page --skip-selector // 新增英雄頁面元件
ng g c pages/edit-hero-page --skip-selector // 編輯英雄頁面元件
ng g c shared/components/hero-information-form // 英雄資訊表單元件

並配置對應的路由,編輯 app-routing.module.ts,將兩個頁面元件配置在路徑 'heroes' 下的 'add'、'edit':

const routes: Routes = [
  {
    path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  {
    path: 'heroes',
    children: [
      {
        path: '',
        component: HeroListComponent
      },
      {
        path: ':id',
        component: HeroDetailComponent,
      },
      {
        path: 'add',
        component: AddHeroPageComponent
      },
      {
        path: 'edit',
        component: EditHeroPageComponent
      }
    ]
  },
]

這時候如果我們啟動應用程式,並輸入路徑 http://localhost:4200/heroes/addhttp://localhost:4200/heroes/edit 會發現畫面一片空白,並且 console 都會出現 response error:

https://ithelp.ithome.com.tw/upload/images/20210925/20128395RlUoPjeSSs.png

發生了什麼事呢?這是因為我們將新增的兩個路由配置到參數路由 :id 之後,因此,接在 heroes 之後的 addedit 均被視為 id 參數,因此導向 HeroDetailComponent 並執行取得個別英雄資料的方法 getHero(heroId),因為後台查詢不到匹配這兩個 id( addedit) 的英雄,因此產生 Response Error。

因為一旦路由匹配成功,就不會繼續往下觀察路徑。所以放在參數路徑之後的路由都是無效的,我們應將新增的兩個路由配置到參數路由之前:

const routes: Routes = [
  {
    path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  {
    path: 'heroes',
    children: [
      {
        path: '',
        component: HeroListComponent
      },
      {
        path: 'add',
        component: AddHeroPageComponent
      },
      {
        path: 'edit',
        component: EditHeroPageComponent
      },
      {
        path: ':id',
        component: HeroDetailComponent,
      },
    ]
  },
]

如此一來,就能正常進入新配置的路由:

https://ithelp.ithome.com.tw/upload/images/20210925/201283957Jet0oIeh6.png

接著讓我們來實作英雄資訊表單這個共用元件。

實作英雄資訊表單元件

目前我們的英雄資料模型如下(hero.model.ts):

export interface Hero {
  id?: number;          // id
  name: string;        // 姓名
  image?: string;      // 圖像
  hp: string;          // 生命值
  attack: number;      // 攻擊力
  defence: number;     // 防禦力
  weapon?: string;     // 武器
  skill?: string;      // 必殺技
  description: string; // 人物介紹
}

這邊作了稍微的調動,我們將 id 屬性給為選擇性的(可以不提供這個屬性)。為什麼呢?這是因為大多時候,id 是由後端產生的,也就是說,在新增英雄時我們不需要傳送 id 屬性。

當然這可能不是一個很好的做法,也許會造成解讀上的誤會(原來英雄可以不用有 id?)。可以採用的方法至少有:

  • 新增另外一個 AddHero 介面,在這個介面中不包含 id 屬性。
  • 將原先的 Hero 屬性刪除 id 屬性,並新增一個 DisplayHero 屬性來繼承 Hero,並擴充出 id 屬性。用它來負責前端資料的模型。
  • ...

不過為了方便演示,目前我們先將 id 屬性作為一個選填屬性,將焦點放在完成表單。

首先,我們要在 AppModule 先匯入 FormsModule,這樣我們才可以使用 Template-driven Form 相關的指令:

import { FormsModule } from '@angular/forms';
(略)
@NgModule({
(略)
  imports: [
    (略)
    FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

我們以 name 屬性為例來討論,在範本驅動表單的模式下,可以寫為下面這樣(hero-information-form.component.html):

  <div class="form-field">
    <label for="name">NAME</label>
    <input
      #tName="ngModel"
      name="name"
      ngModel
      required
      type="text"
      id="name" />
  </div>

解釋一下與 Angular 相關的程式碼。

最核心的就是 ngModel 指令,這個指令會產生一個表單控制項(FormControl)的實例。聽起來就控場控很大,沒錯!表單控制項就是 Angular Form 的場控基石(連歐拉夫都解不了)。表單控制項可以讓我們獲得該欄位的狀態資訊,比較常使用到的包含:

  • value // 現在的值
  • valid/invalid // 是否合法
  • errors // 是否有錯誤
  • touched/untouched // 表單是否已被使用者接觸過(眼神不算)
  • dirty/pristine // 表單是否被編輯過/原初狀態

並且會提供相應於上述表單控制狀態的 class(例如不合法時提供 ng-invalid class),因此,你可以很方便地完成表單狀態樣式的顯示。

當你使用了 ngModel 指令後,你就可以繼續使用檢核相關的指令,例如:

  • required // 此欄位為必填
  • email // 此欄位輸入格式須符合 email
  • minlength // 此欄位最少字元限制
  • maxlength // 此欄位最多字元限制
  • pattern // 此欄位輸入格式須符合指定的正則表達式
  • ...

在姓名欄位,我們使用了 required 檢核指令,標示這是一個必填欄位。

這樣我們就完成了一個表單欄位的設定。但在畫面上,我們常常需要知道檢核狀態,例如需要知道它是否有錯誤、要顯示錯誤訊息。因此我們把這個表單控制項的實例指派給一個範本參考變數(也就是 #tName)。如此一來,我們就可以在 HTML 檔案中,以 tName 來使用表單控制項提供的各種場控技能。

例如我們新增一個「儲存」按鈕,並設置它在這個名稱欄位不合法的時候(沒有填寫)是 disabeld 的:

<div class="form-field">
  <label for="name">NAME</label>
  <input
    #tName="ngModel"
    name="name"
    ngModel
    required
    type="text"
    id="name" />
</div>

<button
  type="button"
  [disabled]="tName.invalid">
  儲存
</button>

我們先在 AddHeroPageCompoent 使用這個表單元件來看看效果:

<h1>新增英雄</h1>
<app-hero-information-form></app-hero-information-form>

畫面如下,在沒有輸入值的時候,按鈕無法點擊的:

https://ithelp.ithome.com.tw/upload/images/20210925/20128395Ya5hcxBcXu.png

當輸入之後,就可以點擊了:

https://ithelp.ithome.com.tw/upload/images/20210925/20128395oMumODLcAH.png

透過範本參考變數(Template variables)#tName 和表單控制項實例(ngModel)的配合,就可以很輕鬆地產出動態檢核欄位。明天會完成這個英雄資訊表單,並優化它的畫面。

今天的程式碼已推上 Github


上一篇
第 9 天 元件還是頁面,這是個問題|page、component
下一篇
第 11 天 範本驅動表單的動態檢核訊息|ngSubmit
系列文
關於我作夢變成工程師這檔事(Angular 篇)14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言