Angular表单通常包含双向数据绑定、变化跟踪、验证、错误处理等。接下来学习如何构建一个简单的表单,包括以下内容:
ngModel
读写input的值ngControl
指令跟踪表单的状态变化和有效性ngControl
指令为表单控件添加CSS类我们要实现一个简单的的注册页面,当有必填项没填时,显示验证信息并禁用提交按钮:
首先,创建一个项目文件夹angular2-forms
,按照[QuickStart]创建项目。
在app
文件夹下新建文件user.ts
,创建一个User
类,其中有三个必填字段(id
、name
、email
),一个可选字段(address
):
export class User {
constructor(
public id: number,
public name: string,
public city: string,
public email?: string
) { }
}
一个Angular表单包含两部分:基于HTML的模板和处理数据及用户交互的组件。
在文件夹app
下创建文件user-form.component.ts
,
import {Component} from 'angular2/core';
import {NgForm} from 'angular2/common';
import { Hero } from './user';
@Component({
selector: 'user-form',
templateUrl: 'app/user-form.component.html'
})
export class UserFormComponent {
cities = ['BeiJing', 'ShangHai',
'NanJing', 'ShenZhen'];
model = new User(18, 'Xiao Hui', this.cities[2], '[email protected]');
submitted = false;
onSubmit() { this.submitted = true; }
// TODO: Remove this when we're done
get diagnostic() { return JSON.stringify(this.model); }
}
修改根组件app.component.ts
,调用新创建的UserForm
组件:
import {Component} from 'angular2/core';
import {HeroFormComponent} from './user-form.component'
@Component({
selector: 'my-app',
template: '<user-form></user-form>',
directives: [UserFormComponent]
})
export class AppComponent { }
在app
文件夹下创建文件user-form.component.html
:
<div class="container">
<h1>User Form</h1>
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" class="form-control">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
这是一个普通的HTML5片段,还没有添加Angular的绑定和指令,只是页面布局。其中,container
、form-group
、form-control
和btn
这些样式来自Twitter Bootstrap。
安装Bootstrap,在项目根文件夹下执行:
npm install bootstrap --save
在index.html
中添加样式文件的引用:
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
用户在表单中使用下拉列表来选择城市,使用NgFor
来绑定数组:
<div class="form-group">
<label for="city">City</label>
<select class="form-control" required>
<option *ngFor="#c of cities" [value]="c">{{c}}</option>
</select>
</div>
运行应用,结果如下:
.png
界面上没有数据,是因为我们还没有绑定User
。前面两篇学习了属性绑定和事件绑定来更新组件属性后和页面的值。这里使用一种新的方法,使用NgModel
指令实现双向数据绑定。
<div class="container">
<h1>User Form</h1>
<form>
{{diagnostic}}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required
[(ngModel)]="model.name" >
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" class="form-control"
[(ngModel)]="model.email">
</div>
<div class="form-group">
<label for="city">City</label>
<select class="form-control" required
[(ngModel)]="model.city" >
<option *ngFor="#c of cities" [value]="c">{{c}}</option>
</select>
</div>
</form>
</div>
运行应用,改变界面上的值。可以看到{{diagnostic}}
中的调试信息也随着改变,说明双向数据绑定生效了。
.png
在属性绑定中,值从模型流向视图中的目标属性,我们将目标属性放在方括号[]
中,这是从模型到视图的单向数据绑定。
在事件绑定中,我们将视图中的目标属性的值流向模型,我们将目标属性放在圆括号()
中,这是从视图到模型的单向数据绑定。
Angular将两者结合起来,形成[()]
,来标识双向数据绑定。
事实上,我们也可以将NgModel
绑定拆分成两部分:
<input type="text" class="form-control" required
[ngModel]="model.name"
(ngModelChange)="model.name = $event" >
这里的属性绑定跟之前介绍的是一样的,但是事件绑定看起来就有点奇怪了。ngModelChange
并不是<input>
元素的事件,它实际上是NgModel
指令的事件属性。当Angular遇到[(x)]
这样的绑定时,它希望指令x
有一个x
输入属性和一个xChange
输出属性。
另一个奇怪的地方是模板表达式model.name = $event
。我们在前面见过来自DOM事件的$event
对象。ngModelChange
属性并不产生DOM事件,它只是一个Angular的EventEmitter
属性,当该事件触发时,它返回的是input文本框的值。
一个表单不仅仅有数据绑定。我们还希望知道表单中控件的状态。
ngControl
指令可以告诉我们用户是否触发了控件、值是否改变了、值是否是无效的。
这个指令不仅仅跟踪状态,它还会使用特殊的Angular的CSS类更新组件。我们可以利用这些CSS类来改变控件的外观或控制控件的显示与隐藏。
我们为表单中的三个input
控件添加ngControl
,例如:
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" >
在这里,我们给ngControl
指定的是name
。其实任何唯一值都可以。
在内部,Angular会创建Controls
并使用<form>
的NgForm
指令将他们注册到ngControl
的名称下。
在我们的模板中,ngControl
属性实际上映射为NgControlName
指令。还有一个NgControl
指令,跟这个不是同一个东西。但是我们通常忽略他们的区别,就把NgControlName
当成NgControl
指令。
在Angular引擎中,双向绑定语法中的ngModel
是NgControlName
指令的一个属性,不再涉及到NgModel
指令。因为我们只需要一个指令来管理DOM元素,具体是哪个指令没什么区别。
NgControl
指令不仅仅跟踪状态,它还会更新CSS类来反映状态。
ng-touched
类,反之ng-untouched
ng-dirty
类,反之ng-pristine
ng-valid
类,反之ng-invalid
例如为Name文本框添加本地模板变量,测试控件在状态发生变化时CSS类的变化:
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" #spy >
<br>TODO: remove this: {{spy.className}}
接下来,我们利用ng-valid | ng-invalid
来做验证提示。效果如图:
添加两个样式来实现上面的效果。在项目根文件夹下创建文件forms.css
:
.ng-valid[required] {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid {
border-left: 5px solid #a94442; /* red */
}
并在index.html
中添加引用:
<link rel="stylesheet" href="forms.css">
上面的提示只指示验证是否有效,如果无效的话,没有给出错误消息。因此我们加上提示消息:
为Name文本框添加本地模板变量,并在它下面添加显示错误信息的<div>
:
<label for="name">Name</label>
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" #name="ngForm" >
<div [hidden]="name.valid || name.pristine" class="alert alert-danger">
Name is required
</div>
这里,我们在控件为valid
或pristine
时隐藏提示信息。pristine
表示页面加载后我们还没有改变控件值的状态。
上面的代码中,本地模板变量的值为ngForm
。为什么是ngForm
呢?一个指令的exportAs
属性用来告诉Angular如何链接本地变量到指令。我们将本地变量赋值为ngForm
是因为,NgControlName
指令的exportAs
属性正好是ngForm
。
这个看起来有点不直观。因为,Angular中的控件指令中,NgForm
、NgModel
、NgControlName
和NgControlGroup
的exportAs
属性都是ngForm
,但是我们只能在控件上使用其中一个指令。
添加一个New User
按钮,,将点击事件绑定到组件的方法:
<button type="button" class="btn btn-default" (click)="newUser()">New User</button>
在组件中添加方法:
newUser() {
this.model = new User(42, '', '');
}
运行应用并点击按钮,必填项的文本框左侧会显示红色,但是不会显示提示信息,因为我们并没有改变控件中的值。
输入名称后再点击按钮,这时候会现实提示信息。为什么会这样呢?我们显示的还是一个空的User也显示提示消息了。
这是因为Name文本框不再是原始状态了。用新的User替换原来的之后,控件的状态并没有被恢复为未修改状态。(Angular不能分辨是model被替换了,还是只清除了name属性的值)。我们必须手动重置表单控件。为组件添加一个active
标记,初始值为true
,当我们添加一个新的User,将active
切换为false
,然后使用setTimeout
立刻将它切换回true
。
active = true;
newUser() {
this.model = new User(42, '', '');
this.active = false;
setTimeout(()=> this.active=true, 0);
}
为form
绑定active
标记:
<form *ngIf="active">
这样,点击按钮后form
会从DOM移除再重新创建,页面会闪一下。这样表单控件都是初始的未修改状态了。
用户填写完表单后应该能提交表单。在页面地步添加一个Submit
按钮,该按钮本身并不做什么,但是它会出发表单提交。因为它的type
为submit
。然后还要在<form>
上使用NgSubmit
指令,绑定到组件的onSubmit()
方法:
<form *ngIf="active" (ngSubmit)="onSubmit()" #userForm="ngForm">
这里,我们又定义了一个本地变量#userForm
,赋值为ngForm
,这样heroForm
就指向管理整个表单的NgForm
指令了。
等等,怎么是NgForm
指令呢?我们根本就没有添加NgForm
指令啊?
这是因为Angular自动为<form>
创建并添加NgForm
指令。
NgForm
指令为form
元素补充了一些额外的特性。它包含使用ngControl
属性的元素,监控它们的属性包括有效性。它本身也有一个valid
属性,当它包含的控件全都有效时为true
。
将Submit按钮的disabled
属性绑定到表单的有效性:
<button type="submit" class="btn btn-default" [disabled]="!userForm.form.valid">Submit</button>
用一个<div>
包装表单,将它的hidden
属性绑定到组件的submitted
属性:
<div [hidden]="submitted">
<h1>User Form</h1>
<form *ngIf="active" (ngSubmit)="onSubmit()" #userForm="ngForm">
<!-- ... all of the form ... -->
</form>
</div>
在组件中添加:
submitted = false;
onSubmit() { this.submitted = true; }
当我们点击Submit按钮,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 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Email</div>
<div class="col-xs-9 pull-left">{{ model.email }}</div>
</div>
<div class="row">
<div class="col-xs-3">City</div>
<div class="col-xs-9 pull-left">{{ model.city }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
我们添加了Edit
按钮,点击后将清除submitted
标志,这样就重新显示之前的可编辑的表单啦。
在浏览器中的效果如下:
点此下载完整代码
参考资料
Angular官方文档