昨日實作其中一個英雄表單欄位「姓名」後,演示了如何使用 FormControl 表單控制項搭配範本參考變數(Template Reference)來掌握欄位的狀態。
今天我們將進一步完整整個英雄資訊表單,包括顯示欄位錯誤資訊(例如,必填欄位尚未填寫的話,會顯示相對的提示)。
最後,再來談談 Angualr 對於表單 submit 事件的處理。
我們一樣藉由必填欄位「名稱」來看看怎麼處理錯誤資訊的顯示,將 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" />
<ng-container *ngIf="tName.invalid">
<div *ngIf="tName.errors?.required">此欄位為必填</div>
</ng-container>
</div>
此時我們進入表單的時候,就會看到這個錯誤提示:
成功了!但先等一等,使用者才剛進去就要看這個提醒嗎?如果有很多欄位不就滿江紅了?
因此我們可以再加上一個表單控制項提供的屬性 touched,也就是當使用者接觸過這個欄位,但仍然沒有輸入值的時候,那麼就會出現紅字警語,這樣就不算冤枉了吧:
<div class="form-field">
(...)
<ng-container *ngIf="tName.invalid && tName.touched">
<div *ngIf="tName.errors?.required">此欄位為必填</div>
</ng-container>
</div>
除了這個處理方式之外,有時候我們也可能想要這樣設計:只有當使用者點擊 submit 按鈕後,才提供檢核提示文字。這時候,我們就可以使用 ngForm 提供的屬性 submitted,因此,我們要在 <form>
我們將 ngForm 賦予給範本參考變數 tForm,而 ngSubmit 事件我們等一下會解釋:
<form #tForm="ngForm" (ngSubmit)="do some thing...">
<div class="form-field">
(...)
<ng-container *ngIf="tName.invalid && tForm.submitted">
<div *ngIf="tName.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<button type="submit">新增英雄</button>
</form>
可以看到,在檢核提示資訊的顯示條件為 *ngIf="tName.invalid && tForm.submitted"
,因此,只有在使用者點擊 submit 按鈕「新增英雄」後,如有不合法的欄位才會顯示紅字提示。
了解錯誤資訊的動態顯示機制後,我們來為表單增加一個「重設」按鈕:
<form #tForm="ngForm" (ngSubmit)="do some thing...">
<button type="submit">新增英雄</button>
<button type="button" (click)="tForm.reset()">重設</button>
</form>
我們在重設按鈕監聽 click 事件,在點擊時會使用 tForm(ngForm)的方法 reset(),除了會清空資料後,也會把檢核狀態(invalid、touched...)都還原。
在 HTML 提交表單,預設的行為是刷新頁面。但這樣的機制在「單頁應用」(Single Page Application)的架構下卻是不合適的,因為「單頁應用」刷新頁面的話,所有的元件、狀態都會刷新,這大概不會是我們預期的結果。讓我們嘗試在表單中加入 submit 按鈕(hero-information-form.component.html
):
<form>
(...)
<button type="submit">新增英雄</button>
</form>
點擊按鈕之後沒有發生任何事,這是因為 Angular 攔截了預設的 submit 事件,所以並不會出現我們擔心的狀況——整個 SPA 應用重建。取而代之的,當點擊 submit 按鈕時,觸發的是 ngSubmit 事件,我們可以在 <form>
標籤上來監聽這個事件,並進一步設定當 ngSumit 事件觸發時,我們要進行什麼行為,例如呼叫 doSubmit 方法。
<form (ngSubmit)="doSubmit()">
(...)
<button type="submit">新增英雄</button>
</form>
當然這樣不會任何事情是,因為我們沒有在 ts 檔實作 doSubmit 方法。此外,我們也沒有將這個表單的值傳給這個方法,我們先在 hero-information-form.component.ts
實作 doSubmit 方法:
doSubmit(hero: Hero): void {
console.log('submit new hero data', hero);
}
接著我們使用範本參考變數(Template Variables)搭配 ngForm 來取得表單現在的值:
<form #tForm="ngForm" (ngSubmit)="doSubmit(tForm.value)">
(...)
<button type="submit">新增英雄</button>
<button type="button" (click)="tForm.reset()">重設</button>
</form>
除外,我們可以在 submit 按鈕「新增英雄」上,針對其 disabled 進行屬性繫結,條件為 [disabled]="tForm.invalid"
。因此,當表單擁有非法的欄位時,就無法點擊這個按鈕,優化使用體驗:
我們先將每個表單加上必填檢核(required),在範本驅動表單預設可設定的檢核指令請參考文件,完成的表單程式碼如下:
<form
#tForm="ngForm"
(ngSubmit)="doSubmit(tForm)">
<div class="form-field">
<label for="name">NAME</label>
<input
#tName="ngModel"
name="name"
ngModel
required
type="text"
id="name" />
<ng-container *ngIf="tName.invalid && tForm.submitted">
<div class="error-message" *ngIf="tName.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<div class="form-field">
<label for="hp">HP</label>
<input
#tHp="ngModel"
name="hp"
ngModel
required
type="number"
id="hp" />
<ng-container *ngIf="tHp.invalid && tForm.submitted">
<div class="error-message" *ngIf="tHp.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<div class="form-field">
<label for="attack">ATTACK</label>
<input
#tAttack="ngModel"
name="attack"
ngModel
required
type="number"
id="attack" />
<ng-container *ngIf="tAttack.invalid && tForm.submitted">
<div class="error-message" *ngIf="tAttack.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<div class="form-field">
<label for="defence">DEFENCE</label>
<input
#tDefence="ngModel"
name="defence"
ngModel
required
type="number"
id="defence" />
<ng-container *ngIf="tDefence.invalid && tForm.submitted">
<div class="error-message" *ngIf="tDefence.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<div class="form-field">
<label for="weapon">WEAPON</label>
<input
#tWeapon="ngModel"
name="weapon"
ngModel
required
type="string"
id="weapon" />
<ng-container *ngIf="tWeapon.invalid && tForm.submitted">
<div class="error-message" *ngIf="tWeapon.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<div class="form-field">
<label for="skill">SKILL</label>
<input
#tSkill="ngModel"
name="skill"
ngModel
required
type="string"
id="skill" />
<ng-container *ngIf="tSkill.invalid && tForm.submitted">
<div class="error-message" *ngIf="tSkill.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<div class="form-field">
<label for="description">
DESCRIPTION
</label>
<textarea
ngModel
required
#tDescription="ngModel"
name="description"
id="description"
cols="30"
rows="10">
</textarea>
<ng-container *ngIf="tDescription.invalid && tForm.submitted">
<div class="error-message" *ngIf="tDescription.errors?.required">此欄位為必填</div>
</ng-container>
</div>
<button
type="submit"
[disabled]="tForm.invalid">
新增英雄
</button>
<button
type="button"
(click)="tForm.reset()">
重設表單
</button>
</form>
完整程式碼已推上 Github。