Angular 构建模板驱动表单
构建模板驱动表单
本教程将为你演示如何创建一个模板驱动表单,它的控件元素绑定到数据属性,并通过输入验证来保持数据的完整性和样式,以改善用户体验。
当在模板中进行更改时,模板驱动表单会使用双向数据绑定来更新组件中的数据模型,反之亦然。
Angular 支持两种交互式表单的设计方法。你可以使用 Angular 中的模板语法和指令,以及本教程中描述的表单专用指令和技巧编写模板来构建表单,或者你可以使用响应式方式(或叫模型驱动方式)来构建表单。
模板驱动表单适用于小型或简单的表单,而响应式表单则更具伸缩性,适用于复杂表单。
你可以用 Angular 模板来构建各种表单,比如登录表单、联系人表单和几乎所有的业务表单。你可以创造性地对控件进行布局并把它们绑定到对象模型的数据上。你可以指定验证规则并显示验证错误,有条不紊地启用或禁用特定控件,触发内置的视觉反馈等等。
本教程将向你展示如何通过一个简化的范例表单来从头构建一个表单,就像“英雄之旅”教程的中用一个表单来讲解这些技巧一样。
目标
本教程将教你如何执行以下操作:
- 使用组件和模板构建一个 Angular 表单
- 使用
ngModel
创建双向数据绑定,以便读写输入控件的值 - 使用跟踪控件状态的特殊 CSS 类来提供视觉反馈
- 向用户显示验证错误,并根据表单状态启用或禁用表单控件
- 使用模板引用变量在 HTML 元素之间共享信息
构建一个模板驱动表单
模板驱动表单依赖于 FormsModule
定义的指令。
指令 |
详细信息 |
---|---|
NgModel
|
会协调其附着在的表单元素中的值变更与数据模型中的变更,以便你通过输入验证和错误处理来响应用户输入。 |
NgForm
|
会创建一个顶级的 |
NgModelGroup
|
会创建 |
范例应用
英雄雇佣管理局使用本指南中的范例表单来维护英雄的个人信息。毕竟英雄也要工作啊。这个表单有助于该机构将正确的英雄与正确的危机匹配起来。
该表单突出了一些易于使用的设计特性。比如,这两个必填字段的左边是绿色条,以便让它们醒目。这些字段都有初始值,所以表单是有效的,并且 Submit 按钮也是启用的。
当你使用这个表单时,你将学习如何包含验证逻辑,如何使用标准 CSS 自定义表达式,以及如何处理错误条件以确保输入的有效性。比如,如果用户删除了英雄的名字,那么表单就会失效。该应用会检测已更改的状态,并以醒目的样式显示验证错误。此外,Submit 按钮会被禁用,输入控件左侧的“必填”栏也会从绿色变为红色。
步骤概述
在本教程中,你将使用以下步骤将一个范例表单绑定到数据并处理用户输入。
- 建立基本表单。
- 定义一个范例数据模型
- 包括必需的基础设施,比如
FormsModule
- 使用
ngModel
指令和双向数据绑定语法把表单控件绑定到数据属性。 - 检查
ngModel
如何使用 CSS 类报告控件状态 - 为控件命名,以便让
ngModel
可以访问它们 - 用
ngModel
跟踪输入的有效性和控件的状态。 - 添加自定义 CSS 来根据状态提供可视化反馈
- 显示和隐藏验证错误信息
- 通过添加到模型数据来响应原生 HTML 按钮的单击事件
- 使用表单的
ngSubmit
输出属性来处理表单提交。 - 在表单生效之前,先禁用 Submit 按钮
- 在提交完成后,把已完成的表单替换成页面上不同的内容
建立表单
你可以根据这里提供的代码从头创建范例应用,也可以查看 现场演练 / 下载范例。
- 这里提供的范例应用会创建一个
Hero
类,用于定义表单中所反映的数据模型。 - 该表单的布局和细节是在
HeroFormComponent
类中定义的。 - 下面的代码会创建一个新的 hero 实例,以便让初始的表单显示一个范例英雄。
- 该应用启用了表单功能,并注册了已创建的表单组件。
- 该表单显示在根组件模板定义的应用布局中。
- Name
<input>
控件元素中包含了 HTML5 的 required
属性 - Alter Ego
<input>
没有控件元素,因为 alterEgo
是可选的 - 范例表单使用的是 Twitter Bootstrap 中的一些样式类:
container
,form-group
,form-control
和 btn
。要使用这些样式,就要在该应用的样式表中导入该库。 - 这份表单让英雄申请人从管理局批准过的固定清单中选出一项超能力。预定义
powers
列表是数据模型的一部分,在 HeroFormComponent
内部维护。Angular 的NgForOf
指令会遍历这些数据值,以填充这个 <select>
元素。
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-form',
templateUrl: './hero-form.component.html',
styleUrls: ['./hero-form.component.css']
})
export class HeroFormComponent {
powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'];
model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');
submitted = false;
onSubmit() { this.submitted = true; }
}
该组件的 selector
值为 “app-hero-form”,意味着你可以用 <app-hero-form>
标签把这个表单放到父模板中。
const myHero = new Hero(42, 'SkyDog',
'Fetch any object at any distance',
'Leslie Rollover');
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
这个演示使用虚拟数据来表达 model
和 powers
。在真正的应用中,你会注入一个数据服务来获取和保存实际数据,或者把它们作为输入属性和输出属性进行公开。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroFormComponent } from './hero-form/hero-form.component';
@NgModule({
imports: [
BrowserModule,
CommonModule,
FormsModule
],
declarations: [
AppComponent,
HeroFormComponent
],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule { }
<app-hero-form></app-hero-form>
初始模板定义了一个带有两个表单组和一个提交按钮的表单布局。表单组对应于 Hero 数据模型的两个属性:name 和 alterEgo。每个组都有一个标签和一个用户输入框。
Submit 按钮里面有一些用于样式化的类。此时,表单布局全都是纯 HTML5,没有绑定或指令。
@import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css');
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power" required>
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
</select>
</div>
如果你现在正在运行该应用,你会看到选择控件中的超能力列表。由于尚未将这些 input 元素绑定到数据值或事件,因此它们仍然是空白的,没有任何行为。
把输入控件绑定到数据属性
下一步是使用双向数据绑定把输入控件绑定到相应的 Hero
属性,这样它们就可以通过更新数据模型来响应用户的输入,并通过更新显示来响应数据中的程序化变更。
该 ngModel
指令是由 FormsModule
声明的,它能让你把模板驱动表单中的控件绑定到数据模型中的属性。当你使用双向数据绑定的语法 [(ngModel)]
引入该指令时,Angular 就可以跟踪控件的值和用户交互,并保持视图与模型的同步。
- 编辑模板
hero-form.component.html
。 - 找到 Name 标签旁边的
<input>
标记。 - 使用双向数据绑定语法
[(ngModel)]="..."
添加 ngModel
指令。
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name">
TODO: remove this: {{model.name}}
这个例子中在每个 input 标记后面都有一个临时的诊断插值
{{model.name}}
,以显示相应属性的当前数据值。本提醒是为了让你在观察完这个双向数据绑定后删除这些诊断行。
访问表单的整体状态
当你导入了 FormsModule
时,Angular 会自动为模板中的 <form>
标签创建并附加一个 NgForm
指令。(因为 NgForm
定义了一个能匹配 <form>
元素的选择器 form
)。
要访问 NgForm
和表单的整体状态,就要声明一个模板引用变量。
- 编辑模板
hero-form.component.html
。 - 为
<form>
标签添加模板引用变量 #heroForm
,并把它的值设置如下。 - 运行该应用。
- 开始在 Name 输入框中输入。
<form #heroForm="ngForm">
模板变量 heroForm
现在是对 NgForm
指令实例的引用,该指令实例管理整个表单。
在添加和删除字符时,你可以看到它们从数据模型中出现和消失。比如:
用来显示插值的诊断行证明了这些值确实从输入框流向了模型,然后再返回。
为控件元素命名
在元素上使用 [(ngModel)]
时,必须为该元素定义一个 name
属性。Angular 会用这个指定的名字来把这个元素注册到父 <form>
元素上的 NgForm
指令中。
这个例子中为 <input>
元素添加了一个 name
属性,并把它的值设置为 “name”,用来表示英雄的名字。任何唯一的值都可以用,但最好用描述性的名称。
- 为Alter Ego和Hero Power添加类似的
[(ngModel)]
绑定和 name
属性。 - 你现在可以移除显示插值的诊断消息了。
- 要想确认双向数据绑定是否在整个英雄模型上都有效,可以在该组件的顶部添加一个带有
json
管道的新文本绑定。json
管道会把数据序列化为字符串。 - 注意,每个
<input>
元素都有一个 id
属性。<label>
元素的 for
属性用它来把标签匹配到输入控件。这是一个标准的 HTML 特性。 - 每个
<input>
元素都有一个必需的 name
属性,Angular 用它来注册表单中的控件。 - 你已经观察到了这种效果,可以删除
{{ model | json }}
的文本绑定了。
表单模板修改完毕后,应如下所示:
{{ model | json }}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo"
[(ngModel)]="model.alterEgo" name="alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power"
required
[(ngModel)]="model.power" name="power">
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
</select>
</div>
如果你现在运行该应用并更改英雄模型的每个属性,该表单可能会显示如下:
通过表单顶部的诊断行可以确认所有的更改都已反映在模型中。
跟踪控件状态
控件上的 NgModel
指令会跟踪该控件的状态。它会告诉你用户是否接触过该控件、该值是否发生了变化,或者该值是否无效。Angular 在控件元素上设置了特殊的 CSS 类来反映其状态,如下表所示。
状态 |
为 TRUE 时的类名 |
为 FALSE 时的类名 |
---|---|---|
该控件已被访问过。 |
ng-touched
|
ng-untouched
|
控件的值已被更改。 |
ng-dirty
|
ng-pristine
|
控件的值是有效的。 |
ng-valid
|
ng-invalid
|
此外,Angular 还会在提交时把 ng-submitted
类应用到 <form>
元素上。这个类不会应用到内部控件上。
你可以用这些 CSS 类来根据控件的状态定义其样式。
观察控件状态
要想知道框架是如何添加和移除这些类的,请打开浏览器的开发者工具,检查代表英雄名字的 <input>
- 使用浏览器的开发者工具,找到与 “Name” 输入框对应的
<input>
元素。除了 “form-control” 类之外,你还可以看到该元素有多个 CSS 类。 - 当你第一次启动它的时候,这些类表明它是一个有效的值,该值在初始化或重置之后还没有改变过,并且在该控件自初始化或重置后也没有被访问过。
- 在 Name
<input>
框中执行以下操作,看看会出现哪些类。 - 查看,但不要碰它。这些类表明它没有被碰过、还是最初的值,并且有效。
- 在 Name 框内单击,然后单击它外部。该控件现在已被访问过,该元素具有
ng-touched
类,取代了 ng-untouched
类。 - 在名字的末尾添加斜杠。现在它被碰过,而且是脏的(变化过)。
- 删掉这个名字。这会使该值无效,所以
ng-invalid
类会取代 ng-valid
类。
<input … class="form-control ng-untouched ng-pristine ng-valid" …>
为状态创建视觉反馈
注意 ng-valid
/ ng-invalid
这两个类,因为你想在值无效时发出强烈的视觉信号。你还要标记必填字段。
你可以在输入框的左侧用彩条标记必填字段和无效数据:
要想用这种方式修改外观,请执行以下步骤。
- 为
ng-*
CSS 类添加一些定义。 - 把这些类定义添加到一个新的
forms.css
文件中。 - 把这个新文件添加到项目中,作为
index.html
的兄弟: - 在
index.html
文件中,更新 <head>
标签以包含新的样式表。
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
<link rel="stylesheet" href="assets/forms.css">
显示和隐藏验证错误信息
Name 输入框是必填的,清除它就会把彩条变成红色。这表明有些东西是错的,但是用户并不知道要怎么做或该做什么。你可以通过查看和响应控件的状态来提供有用的信息。
当用户删除该名字时,该表单应如下所示:
Hero Power 选择框也是必填的,但它不需要这样的错误处理,因为选择框已经把选择限制在有效值范围内。
要在适当的时候定义和显示错误信息,请执行以下步骤。
- 使用模板引用变量扩展
<input>
标签,你可以用来从模板中访问输入框的 Angular 控件。在这个例子中,该变量是 #name="ngModel"
。 - 添加一个包含合适错误信息
<div>
- 通过把
name
控件的属性绑定到 <div>
元素的 hidden
属性来显示或隐藏错误信息。 - 为
name
输入框添加一个有条件的错误信息,如下例所示。
模板引用变量(
#name
)设置为 "ngModel"
,因为 "ngModel" 是 NgModel.exportAs
属性的值。这个属性告诉 Angular 如何把引用变量和指令链接起来。
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#name="ngModel">
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
Name is required
</div>
关于 "PRISTINE"(原始)状态的说明
在这个例子中,当控件是有效的(valid)或者是原始的(pristine)时,你会隐藏这些消息。原始表示该用户在此表单中显示的值尚未更改过。如果你忽略了 pristine
状态,那么只有当值有效时才会隐藏这些消息。如果你把一个新的(空白)英雄或一个无效的英雄传给这个组件,你会立刻看到错误信息,而这时候你还没有做过任何事情。
你可能希望只有在用户做出无效更改时,才显示该消息。因此当 pristine
状态时,隐藏这条消息就可以满足这个目标。当你在下一步中为表单添加一个新的英雄时,就会看到这个选择有多重要。
添加一个新英雄
本练习通过添加模型数据,展示了如何响应原生 HTML 按钮单击事件。要让表单用户添加一个新的英雄,就要添加一个能响应 click 事件的 New Hero 按钮。
- 在模板中,把 “New Hero” 这个
<button>
元素放在表单底部。 - 在组件文件中,把创建英雄的方法添加到英雄数据模型中。
- 把按钮的 click 事件绑定到一个创建英雄的方法
newHero()
上。 - 再次运行该应用,单击 New Hero 按钮。
- 输入一个名字,然后再次点击 New Hero。
- 要恢复表单控件的原始状态,可以在调用
newHero()
方法之后强制调用表单的 reset()
方法以清除所有标志。
newHero() {
this.model = new Hero(42, '', '');
}
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
表单会清空,输入框左侧的必填栏会显示红色,说明 name
和 power
属性无效。请注意,错误消息是隐藏的。这是因为表单处于原始状态。你还没有改过任何东西。
现在,该应用会显示一条错误信息 Name is required
,因为该输入框不再是原始状态。表单会记住你在单击 New Hero 之前输入过一个名字。
<button type="button" class="btn btn-default" (click)="newHero(); heroForm.reset()">New Hero</button>
现在单击 New Hero 会重置表单及其控件标志。
使用 ngSubmit 提交表单
用户应该可以在填写之后提交这个表单。表单底部的 Submit 按钮本身没有任何作用,但由于它的类型(type="submit"
),它会触发一个表单提交事件。要响应此事件,请执行以下步骤。
- 把表单的
ngSubmit
事件属性绑定到一个 hero-form 组件的 onSubmit()
方法中。 - 使用模板引用变量
#heroForm
访问包含 Submit 按钮的表单,并创建一个事件绑定。你可以把表示它整体有效性的 form 属性绑定到 Submit 按钮的 disabled
属性上。 - 运行该应用。注意,该按钮已启用 - 虽然它还没有做任何有用的事情。
- 删除名称值。这违反了“必需”规则,因此会显示错误消息,并注意它还会禁用“提交”按钮。
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
<button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button>
你不必把按钮的启用状态明确地关联表单的有效性上。当 FormsModule
在增强的表单元素上定义模板引用变量时,会自动执行此操作,然后在按钮控件中引用该变量。
响应表单提交
要展示对表单提交的响应,你可以隐藏数据输入区域并就地显示其它内容。
- 把整个表单包裹进一个
<div>
中并把它的 hidden
属性绑定到 HeroFormComponent.submitted
属性上。 - 主表单从一开始就是可见的,因为在提交之前,它的
submitted
属性都是 false,正如 HeroFormComponent
中的这个片段所显示的: - 点击 Submit 按钮后,
submitted
标志就变为 true
,表单就会消失。 - 要在表单处于已提交状态时显示其它内容,请在新的
<div>
包装器下添加以下 HTML。 - 单击 Edit 按钮,将显示切换回可编辑的表单。
<div [hidden]="submitted">
<h1>Hero Form</h1>
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
<!-- ... all of the form ... -->
</form>
</div>
submitted = false;
onSubmit() { this.submitted = true; }
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9">{{ model.power }}</div>
</div>
<br>
<button type="button" class="btn btn-primary" (click)="submitted=false">Edit</button>
</div>
这个 <div>
(用于显示带插值绑定的只读英雄)只在组件处于已提交状态时才会出现。
另外还显示了一个 Edit 按钮,它的 click 事件绑定到了一个清除 submitted
标志的表达式。
总结
本页讨论的 Angular 表单利用了下列框架特性来支持数据修改,验证等工作。
- 一个 Angular HTML 表单模板
- 带
@Component
装饰器的表单组件类 - 绑定到
NgForm.ngSubmit
事件属性来处理表单提交 - 模板引用变量,比如
#heroForm
和 #name
- 双向数据绑定的
[(ngModel)]
语法 -
name
属性的用途是验证和表单元素的变更跟踪 - 用输入控件上的引用变量的
valid
属性来检查控件是否有效,并据此显示或隐藏错误信息 - 用
NgForm
的有效性来控制 Submit 按钮的启用状态 - 自定义 CSS 类,为用户提供关于无效控件的视觉反馈
这里是该应用最终版本的代码:
- hero-form/hero-form.component.ts
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-form',
templateUrl: './hero-form.component.html',
styleUrls: ['./hero-form.component.css']
})
export class HeroFormComponent {
powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'];
model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');
submitted = false;
onSubmit() { this.submitted = true; }
newHero() {
this.model = new Hero(42, '', '');
}
}
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form</h1>
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#name="ngModel">
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
Name is required
</div>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo"
[(ngModel)]="model.alterEgo" name="alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power"
required
[(ngModel)]="model.power" name="power"
#power="ngModel">
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
</select>
<div [hidden]="power.valid || power.pristine" class="alert alert-danger">
Power is required
</div>
</div>
<button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default" (click)="newHero(); heroForm.reset()">New Hero</button>
</form>
</div>
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9">{{ model.power }}</div>
</div>
<br>
<button type="button" class="btn btn-primary" (click)="submitted=false">Edit</button>
</div>
</div>
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroFormComponent } from './hero-form/hero-form.component';
@NgModule({
imports: [
BrowserModule,
CommonModule,
FormsModule
],
declarations: [
AppComponent,
HeroFormComponent
],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule { }
<app-hero-form></app-hero-form>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent { }
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}