表單是商業應用的支柱,我們用它來執行登錄、求助、下單、預訂機票、安排會議,以及不計其數的其它數據錄入任務。
在開發表單時,創建數據方面的體驗是非常重要的,它能指引用戶明細、高效的完成工作流程。
開發表單需要設計能力(那超出了本章的范圍),而框架支持雙向數據綁定、變更檢測、驗證和錯誤處理,在本章你將會學習它們。
本章展示了如何從草稿構建一個簡單的表單。在這個過程中你將學會如何:
- 用組件和模板構建 Angular 表單。
- 用
ngModel
創建雙向數據綁定,以讀取和寫入輸入控件的值。 - 跟蹤狀態的變化,并驗證表單控件。
- 使用特殊的 CSS 類來跟蹤控件的狀態并給出視覺反饋。
- 向用戶顯示驗證錯誤提示,以及啟用/禁用表單控件。
- 使用模板引用變量在 HTML 元素之間共享信息。
模板驅動表單
你可以使用 Angular 模板語法編寫模板,結合本章所描述的表單專用指令和技術來構建表單。
你還可以使用響應式(也叫模型驅動)的方式來構建表單。不過本章中只介紹模板驅動表單。
利用 Angular 模板,可以構建幾乎所有表單——登錄表單、聯系人表單, 以及任何非常漂亮的商務表單。可以創造性的擺放各種控件、把它們綁定到數據、指定校驗規則、顯示校驗錯誤、有條件的禁用或啟用特定的控件、觸發內置的視覺反饋等等,不勝枚舉。
Angular 通過處理大量重復的、模板化的任務,簡化了過程,從而使你不必陷入與自己的斗爭中。
你將學習構建如下的“模板驅動”表單:
英雄職業介紹所,使用這個表單來維護英雄們的個人信息。每個英雄都需要一份工作。公司的使命就是讓合適的英雄去應對合適的危機。
表單中的三個字段,其中兩個是必填的。根據 material design 指南,必填的字段用星號(*)標出。
如果刪除了英雄的名字,表單就會用醒目的樣式把驗證錯誤顯示出來。
注意,提交按鈕被禁用了,而且輸入控件從綠色變為了紅色。
你將一小步一小步地構建此表單:
- 創建
Hero
模型類。 - 創建控制此表單的組件。
- 創建具有初始表單布局的模板。
- 使用 ngModel 雙向數據綁定語法把數據屬性綁定到每個表單輸入控件。
- 為每個表單輸入控件添加 ngControl 指令。
- 添加自定義 CSS 來提供視覺反饋。
- 顯示和隱藏有效性驗證的錯誤信息。
- 使用 ngSubmit 處理表單提交。
- 禁用此表單的提交按鈕,直到表單變為有效。
配置
根據配置的說明創建一個名為forms
的新項目。
添加 angular_forms
Angular 表單的功能在 angular_forms 庫中,它有自己的包,添加包到 pub 依賴中:
// {quickstart → forms}/pubspec.yaml
dependencies:
angular: ^5.0.0-alpha
+ angular_forms: ^2.0.0-alpha
創建模型
當用戶輸入表單數據時,需要捕獲它們的變化,并更新到模型的實例中。除非知道模型的樣子,否則無法設計表單的布局。
最簡單的模型是個“屬性包”,用來存放關于應用重點的資料。這里使用了描述Hero
類的三個必備字段 (id
、name
、power
),和一個可選字段 (alterEgo
)。
在lib
目錄,按照已給出的內容創建下面的文件:
// lib/src/hero.dart
class Hero {
int id;
String name, power, alterEgo;
Hero(this.id, this.name, this.power, [this.alterEgo]);
String toString() => '$id: $name ($alterEgo). Super power: $power';
}
這是一個少量需求和零行為的貧血模型。對演示來說足夠了。
alterEgo
是可選的,所以構造函數允許你省略它;注意在[this.alterEgo]
中的方括號。
可以像這樣創建新英雄:
var myHero = new Hero(
42, 'SkyDog', 'Fetch any object at any distance', 'Leslie Rollover');
print('My hero is ${myHero.name}.'); // "My hero is SkyDog."
創建基本的表單
Angular 表單分為兩部分:基于 HTML 的模板 ,以及用來處理數據和用戶動態交互的組件類。先從這個類開始,是因為它可以簡要說明英雄編輯器能做什么。
創建表單組件
根據已給出的內容創建下面的文件:
// lib/src/hero_form_component.dart (v1)
import 'package:angular/angular.dart';
import 'package:angular_forms/angular_forms.dart';
import 'hero.dart';
const List<String> _powers = [
'Really Smart',
'Super Flexible',
'Super Hot',
'Weather Changer'
];
@Component(
selector: 'hero-form',
templateUrl: 'hero_form_component.html',
directives: [coreDirectives, formDirectives],
)
class HeroFormComponent {
Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet');
bool submitted = false;
List<String> get powers => _powers;
void onSubmit() => submitted = true;
}
這個組件沒有什么特別的地方,沒有表單相關的東西,與之前寫過的組件沒什么不同。
只需要前面章節中學過的 Angular 概念,就可以完全理解這個組件:
- 這段代碼導入了 Angular 核心庫以及你剛剛創建的
Hero
模型。 -
@Component
選擇器hero-form
表示可以用<hero-form>
元素把這個表單放進父模板。 -
templateUrl
屬性指向一個獨立的 HTML 模板文件(稍后創建)。 - 從
model
和powers
定義模擬數據。
接下來,你可以注入一個數據服務,以獲取或保存真實的數據,或者把這些屬性暴露為輸入屬性和輸出屬性(參見模板語法中的輸入和輸出屬性)來綁定到一個父組件。這不是現在需要關心的問題,未來的更改不會影響到這個表單。
修改 app component
AppComponent
是應用的根組件,HeroFormComponent
將被放在其中。
使用下面的內容替換初始版本:
// lib/app_component.dart
import 'package:angular/angular.dart';
import 'src/hero_form_component.dart';
@Component(
selector: 'my-app',
template: '<hero-form></hero-form>',
directives: [HeroFormComponent],
)
class AppComponent {}
創建初始 HTML 表單模板
使用下面的內容創建模板文件:
// lib/src/hero_form_component.html (start)
<div class="container">
<h1>Hero Form</h1>
<form>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo">
</div>
<div class="row">
<div class="col-auto">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
<small class="col text-right">* Required</small>
</div>
</form>
</div>
這是一段簡單的 HTML5 代碼。我們展現了Hero
的兩個字段,name
和alterEgo
,提供給用戶在輸入框中輸入。
Name 的<input>
控件具有 HTML5 的required
屬性;Alter Ego 的<input>
控件沒有,因為alterEgo
是可選的。
在底部添加了一個具有一些 CSS 類的提交按鈕。
你還沒有用到 Angular。沒有綁定,沒有額外的指令,只有布局。
在模板驅動表單中,你只要導入了
angular_forms
庫,就不用對<form>
做任何其它的事情來使用庫的功能。接下來你會看到它的原理。
刷新瀏覽器。你會看到一個簡單的,沒有樣式的表單。
給表單添加樣式
container
和btn
類都來自 Bootstrap。Bootstrap 也有特定的表單類,包括form-control
和form-group
。它們給表單添加了一點樣式。
Angular 不需要使用 Bootstrap 類或任意外部庫的樣式。Angular 應用可以使用任意 CSS 庫或一點也不用。
在index.html
的<head>
插入下面的鏈接來添加 Bootstrap 樣式。
// web/index.html (bootstrap)
<link rel="stylesheet" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
刷新瀏覽器。你會看到一個帶有樣式的表單。
使用 *ngFor 添加 powers
英雄必須從認證過的固定列表中選擇一項超能力。你在內部維護這個列表(在HeroFormComponent
)。
在表單中添加select
,用ngFor
把powers
列表綁定到列表選項,在之前的顯示數據一章中使用過的技術。
在緊跟著 Alter Ego 組的下方添加如下 HTML:
// lib/src/hero_form_component.html (powers)
<div class="form-group">
<label for="power">Hero Power *</label>
<select class="form-control" id="power" required>
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
powers
列表中的每一項超能力都會渲染成<option>
標簽。 模板輸入變量p
在每個迭代指向不同的超能力,使用雙花括號插值表達式語法來顯示它的名稱。
使用 ngModel 雙向數據綁定
現在運行此應用,有點令人失望。
你看不到英雄數據因為還沒有綁定到Hero
。在前面的章節我們知道怎么去做。顯示數據介紹了屬性綁定。用戶輸入展示了如何通過事件綁定來監聽 DOM 事件,以及如何用顯示的值更新組件的屬性。
現在,需要同時進行顯示、監聽和提取。
雖然可以在表單中再次使用這些已知的技術。但是,你將使用新的[(ngModel)]
語法,使表單綁定到模型的工作變得更容易。
找到Name
對應的<input>
標簽,并且像這樣更新它:
// lib/src/hero_form_component.html (name)
<!-- TODO: remove the next diagnostic line -->
<mark>{{model.name}}</mark><hr>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" class="form-control" id="name" required
[(ngModel)]="model.name"
ngControl="name">
</div>
在 form-group 標簽前添加用于診斷的插值表達式,以看清正在發生什么事。給自己留個注釋,提醒你完成后移除它。
聚焦到綁定語法:[(ngModel)]="..."
上。
現在運行應用,開始在Name 輸入框中鍵入,添加和刪除字符,我們將看到它們從診斷文本中顯示和消失。某一瞬間,它看起來可能是這樣:
診斷信息可以證明,數據確實從輸入框流動到模型,再反向流動回來。
這就是雙向數據綁定!。更多信息,參見模板語法章節的使用 NgModel 雙向綁定。
注意,<input>
標簽還添加了ngControl
指令,并設置為 "name",表示英雄的名字。使用任何唯一的值都可以,但使用具有描述性的“name”會更有幫助。當在表單組合中使用[(ngModel)]
時,必須要定義ngControl
指令。
在內部,Angular 創建了一些
NgFormControl
,并把它們注冊到NgForm
指令,再將該指令附加到<form>
標簽。每個NgFormControl
都都以你分配給NgFormControl
指令的名稱注冊。稍后會看到更多 NgForm 的信息。
為 Alter Ego 和 Hero Power 添加類似的[(ngModel)]
綁定和ngControl
指令。
使用model
替換診斷綁定表達式。這樣就能確認雙向數據綁定在整個 Hero 模型上都能正常工作了。
修改之后,這個表單的核心是這樣的:
// lib/src/hero_form_component.html (controls)
<!-- TODO: remove the next diagnostic line -->
<mark>{{model}}</mark><hr>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" class="form-control" id="name" required
[(ngModel)]="model.name"
ngControl="name">
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo"
[(ngModel)]="model.alterEgo"
ngControl="alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power *</label>
<select class="form-control" id="power" required
[(ngModel)]="model.power"
ngControl="power">
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
- 每個 input 元素都有
id
屬性,label
元素的for
屬性用它來匹配到對應的輸入控件。- 每個 input 元素都有
ngControl
指令,這是 Angular 表單注冊表單控件所必須的。
如果現在運行本應用,修改每個 Hero 模型的屬性,表單可能顯示如下:
表單頂部的診斷信息證實了你所做的一切更改都反映在了 model 中。
從模板刪除診斷綁定因為它已經完成了它的使命。
基于控件狀態提供視覺反饋
使用 CSS 和類綁定,可以改變表單控件的外觀來反映它的狀態。
追蹤控件狀態
一個 Angular 表單控件可以告訴你,用戶是否碰過此控件,值是否發生改變,以及值是否無效。
Angular 表單的每個控件(NgControl)追蹤自身的狀態,并通過檢查下面的成員字段使狀態可用:
-
dirty
和pristine
表明控件的值是否發生改變。 -
touched
和untouched
表明控件是否被訪問。 -
valid
反映了控件值的有效性。
控件樣式
valid
控件屬性是最引人注意的,因為當控件的值無效時,你希望發出強烈的視覺信號。要創建這樣的視覺反饋,你需要使用 Bootstrap custom-forms 的類is-valid
和is-invalid
。
在Name 的<input>
標簽添加一個名為name
的模板引用變量。使用name
和類綁定有條件的指定恰當的表單有效性的類。
給Name 的<input>
標簽臨時添加另一個名為spy
的模板引用變量,用來顯示輸入框的 CSS 類。
// lib/src/hero_form_component.html (name)
<input type="text" class="form-control" id="name" required
[(ngModel)]="model.name"
#name="ngForm"
#spy
[class.is-valid]="name.valid"
[class.is-invalid]="!name.valid"
ngControl="name">
<!-- TODO: remove the next diagnostic line -->
{{spy.className}}
模板引用變量
spy
模板引用變量綁定到了<input>
DOM元素,然而name
變量(#name="ngForm"
)綁定到了與 input 元素相關聯的 NgModel。為什么是 “ngForm”?指令的 exportAs 屬性告訴 Angular 如何鏈接模板引用變量到指令。把
name
設置為 “ngForm” 是因為 ngModel 指令的exportAs
屬性是 “ngForm”。
刷新瀏覽器,遵循下面的步驟:
- 看 Name 輸入框。
- 它有一個綠色的邊框。
- 它有
form-control
和is-valid
類。
- 添加一些字符來改變 name。類名依然不變。
- 刪除 name。
- 輸入邊框變紅。
-
is-invalid
類變成了is-valid
。
刪除#spy
模板引用變量和使用到它的診斷信息。
另一種類綁定的方法,可以使用 NgClass 指令給控件添加樣式。首先添加下面的方法來設置控件的狀態依賴 CSS 類名:
// lib/src/hero_form_component.dart (setCssValidityClass)
Map<String, bool> setCssValidityClass(NgControl control) {
final validityClass = control.valid == true ? 'is-valid' : 'is-invalid';
return {validityClass: true};
}
使用上面方法返回的 map 值,綁定到 NgClass 指令——更多關于這個指令及其替代品的信息請看模板語法章節。
// lib/src/hero_form_component.html (power)
<select class="form-control" id="power" required
[(ngModel)]="model.power"
#power="ngForm"
[ngClass]="setCssValidityClass(power)"
ngControl="power">
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
顯示和隱藏驗證錯誤信息
你可以改進表單。Name 輸入框是必填的,清空它會使輸入框變紅。這表明有些東西錯了,但用戶不知道錯在哪里,或者如何糾正。利用控件狀態來顯示有用的信息。
使用 valid 和 pristine 狀態
當用戶刪除姓名時,表單看起來應該是這樣的:
要達到這個效果,在緊跟著 Name <input>
標簽后面添加下面的<div>
:
// lib/src/hero_form_component.html (hidden error message)
<div [hidden]="name.valid || name.pristine" class="invalid-feedback">
Name is required
</div>
刷新瀏覽器,刪除輸入框中的Name。錯誤信息就顯示出來了。
基于name
控件的狀態,通過設置div
的 hidden 屬性,顯式地控制錯誤信息。
在這個例子中,當控件是 valid 或 pristine 時,隱藏消息。 “pristine” 意味著從它被顯示在表單中開始,用戶還從未修改過它的值。
用戶體驗取決于開發人員的選擇
有些開發人員會希望任何時候都顯示這條消息。如果忽略了
pristine
狀態,就會只在值有效時隱藏此消息。如果往這個組件中傳入全新(空)的英雄,或者無效的英雄,將立刻看到錯誤信息 —— 雖然你還什么都沒做。有些開發人員會希望只有在用戶做出無效的更改時才顯示這個消息。 如果當控件是 “pristine” 狀態時也隱藏消息,就能達到這個目的。在往表單中添加新英雄時,將看到這種選擇的重要性。
Alter Ego 是可選項,所以不用改它。
英雄的超能力選項是必填的。只要愿意,可以往<select>
上添加相同的錯誤處理。但沒有必要,這個選擇框已經限制了“超能力”只能選有效值。
添加清除按鈕
在組件類中添加clear()
方法:
// lib/src/hero_form_component.dart (clear)
void clear() {
model.name = '';
model.power = _powers[0];
model.alterEgo = '';
}
在 提交 按鈕的右邊添加一個綁定click
事件的 Clear 按鈕:
// lib/src/hero_form_component.html (Clear button)
<button (click)="clear()" type="button" class="btn">
Clear
</button>
刷新瀏覽器。點擊 Clear 按鈕。文本域都被清空,如果之前改變了Power 的值,也會重置為默認值。
使用 ngSubmit 提交表單
在填表完成之后,用戶應該能提交這個表單。Submit 按鈕位于表單的底部,它自己不做任何事,但因為它的 type 值 (type="submit"
),所以會觸發表單提交。
此時提交表單是無意義的。為了讓它有意義,指定表單組件的onSubmit()
方法,綁定到表單的ngSubmit
事件綁定:
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
注意模板引用變量#heroForm
。正如前面所說的,變量heroForm
被綁定到管理整個表單的NgForm
指令。
NgForm指令
Angular 自動創建并添加 NgForm 指令到
<form>
標簽。
NgForm
指令給表單元素附加了額外的特性。它會控制那些帶有ngModel
和ngControl
指令的控件元素,監聽他們的屬性,包括其有效性。
你要把表單的總體有效性通過heroForm
變量綁定到按鈕的disabled
屬性上。
<button [disabled]="!heroForm.form.valid" type="submit" class="btn btn-primary">
Submit
</button>
刷新瀏覽器。你會發現按鈕是可用的——盡管它還沒有做任何有用的事。
現在如果我們刪除 Name,就違反了“required”規則,它會恰當的顯示在錯誤信息中。提交 按鈕也被禁用了。
不覺得了不起嗎?再想一想。如果沒有 Angular 的幫助,我們又該怎樣讓按鈕的禁用/啟用狀態和表單的有效性關聯起來呢?
有了 Angular,它就是這么簡單:
- 在(增強的)form 元素上定義一個模板引用變量。
- 在許多行之外的按鈕上引用該變量。
顯示模型 (可選)
此時提交表單沒有視覺特效。
改進 demo 也無法教給我們任何關于表單的新知識。但這是一個練習新學到的綁定技能的好機會。如果你不感興趣,跳到本章的總結部分。
作為視覺效果,隱藏掉數據輸入區域并顯示一些其它東西。
把表單包裹進<div>
中,并把它的hidden
屬性綁定到HeroFormComponent.submitted
屬性。
// lib/src/hero_form_component.html (excerpt)
<div [hidden]="submitted">
<h1>Hero Form</h1>
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
</form>
</div>
表單從一開始就是可見的,因為submitted
屬性是 false,直到我們提交了這個表單,正如下面這段HeroFormComponent
的代碼展示的:
// lib/src/hero_form_component.dart (submitted)
bool submitted = false;
void onSubmit() => submitted = true;
現在,在剛寫的<div>
包裹層下方,添加如下 HTML:
// lib/src/hero_form_component.html (submitted)
<div [hidden]="!submitted">
<h1>Hero data</h1>
<table class="table">
<tr>
<th>Name</th>
<td>{{model.name}}</td>
</tr>
<tr>
<th>Alter Ego</th>
<td>{{model.alterEgo}}</td>
</tr>
<tr>
<th>Power</th>
<td>{{model.power}}</td>
</tr>
</table>
<button (click)="submitted=false" class="btn btn-primary">Edit</button>
</div>
刷新瀏覽器,并提交表單。submitted
標記變成 true,表單消失了。你會看到英雄模型的值(只讀)顯示在表格中。
這個視圖包含一個 Edit 按鈕,它的點擊事件綁定清除submitted
標志。當你點擊 Edit 按鈕時,表格消失,可編輯的表單又出現了。
總結
Angular 表單提供了數據修改、驗證等支持。在本章中學到了怎樣使用下面的特性:
- 一個 HTML 表單模板和一個帶有
@Component
注解的表單組件類。 - 通過一個
ngSubmit
事件綁定處理表單提交。 - 模板引用變量,例如
heroForm
和name
。 - 雙向數據綁定(
[(ngModel)]
)。 - 用于驗證和追蹤表單元素變化的
ngControl
指令。 - input 控件的
valid
屬性(通過模板引用變量獲取),用于檢查控件的有效性和顯示/隱藏錯誤信息。 - NgForm.form的有效性來設置 提交 按鈕的激活狀態。
- 定制 CSS 類來給用戶提供控件狀態的視覺反饋。
下一步