表单是商业应用的支柱,我们用它来执行登录、求助、下单、预订机票、安排会议,以及不计其数的其它数据录入任务。
在开发表单时,创建数据方面的体验是非常重要的,它能指引用户明细、高效的完成工作流程。
开发表单需要设计能力(那超出了本章的范围),而框架支持双向数据绑定、变更检测、验证和错误处理,在本章你将会学习它们。
本章展示了如何从草稿构建一个简单的表单。在这个过程中你将学会如何:
- 用组件和模板构建 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" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" 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 类来给用户提供控件状态的视觉反馈。
下一步