Angular2表单模板驱动的表单(Template-Driven Forms)
http://codin.im/2016/09/26/angular2-form-template-driven/
在网页开发中,表单估计是最常用的一个,同时也是最麻烦、最容易出问题的。在一个稍微复杂一点的应用中,我们除了用表单元素收集数据,还需要验证,几个数据之间可能还会相互关联,然后根据不同的数据值调用不同的业务逻辑等。
使用Angular提供的数据绑定的功能,我们可以很容易就在组件中获得用户输入的数据,Angular也提供了几种验证方式方便我们进行数据的校验。但是,一些自定义的数据验证、数据交互和业务逻辑还是需要自己处理。
在Angular2中,提供了2种表单实现方式,分别是’template-driven’(模板驱动的表单)和’model-driven’(模型驱动表单)。在这篇文章中,我们先来看看模板驱动的表单。顾名思义,模板驱动的表单就是大部分表单相关代码都在模板里,通过在模板里面添加ngForm, ngModel和ngModelGroup等属性来定义模板和验证信息,以及它跟组件之间的数据交互。
实例
下图是这篇文章使用的实例的界面:
它是一个用户信息输入的表单,包括4个字段,用户名、电话、城市和街道,演示了如何使用表单,给各个字段添加验证并显示验证结果,以及如何在组件中判断是否出错并获取出错信息。
项目源码可以从github获取,这个项目包含了几个Angular2表单相关的实例,可以使用下面的命令获取本文所对应的代码:
1
|
git clone https://github.com/Mavlarn/angular2-forms-tutorial
|
然后进入项目目录,运行下面的命令安装依赖然后运行测试服务器:
1 2 3 4
|
cd angular2-forms-tutorial git checkout template-driven # 检出该文所使用的tag npm install npm start
|
该项目是基于之前的Angular2-basic模板,这个教程相关的代码都在’template-forms’目录里面。
引入FormsModule
首先,我们需要在app.module.ts里引入FormsModule。
1 2 3 4 5 6
|
import { FormsModule } from '@angular/forms'; //省略其他 @NgModule({ imports: [ BrowserModule, FormsModule ], //省略其他 })
|
初始表单
然后,我们从一个基本的html表单开始:
1 2 3 4 5 6 7 8 9 10 11
|
<form> <label>姓名:</label> <input type="text"> <label>电话:</label> <input type="text"> <label>城市:</label> <input type="text"> <label>街道:</label> <input type="text"> <button type="submit">保存</button> </form>
|
在实际的实例中,使用了bootstrap的表单样式,一组输入框应该是下面这个样子,但是在本文中,为了节省页面显示的篇幅,我省略了div, form-group等,我们只需要关心如何在Angular2中使用模板驱动的表单。如果想查看完整的带样式的代码,请查看源文件。
1 2 3 4 5 6
|
<div class="form-group"> <label class="col-sm-2 control-label">姓名:</label> <div class="col-sm-10"> <input class="form-control" type="text"> </div> </div>
|
ngForm
在上面的表单里,我们没有使用Angular2的任何功能,如数据绑定,也没有使用其他指令。但是,Angular2在<form>
上实现了一个指令’ngForm’,这样,对于所有的html的form表单,都会使用ngForm组件去初始化该表单。
使用ngForm对象
接下来,我们需要在模板里面访问这个ngForm的实例,这样我们就能够从这个实例里面获取数据,或者获取数据验证状态。
在Angular2里,都提供了一个模板引用变量
的功能,通过#
加变量实现。通过这个功能,我们可以在同一元素、兄弟元素或任何子元素中引用模板引用变量。这样听着还是不好理解,我们看一个例子:
1 2
|
<input #phone placeholder="phone number"> <button (click)="callPhone(phone.value)">Call</button>
|
在这个例子中,我们通过#phone
定义了一个变量,它所指的就是这个input元素,phone.value
也就是这个输入框输入的值。
除了使用
#
,也可以使用ref-
,例如ref-phone
形式的定义跟#phone
是一样的。
我们可以对任何的DOM元素使用这种方式获取当前引用,也可以对任何的Angular2的指令使用。在这个表单的例子中,我们这样来获取这个ngFrom的引用:
1
|
<form #userForm="ngForm">
|
其中’ngForm’就是当前这个指令,这样在这个模板里面,我们可以用userForm
获得表单的所有数据。
提交表单
在html中,我们要提交一个form,会在form里写一个action的属性,然后,用一个类型为’submit’的按钮来提交。但是,在Angular2中,我们需要使用ngSubmit
事件:
1 2 3
|
<form #userForm="ngForm" (ngSubmit)="logForm(userForm)"> <button type="submit">保存</button> </form>
|
这样,当用户点击保存按钮的时候,Angular2会使用自己的验证机制,验证所有的数据,然后在调用’logForm(userForm)’方法。
在我们的组件中,实现这个方法:
1 2 3 4 5 6
|
logForm(theForm: NgForm) { console.log(theForm.value); if (theForm.invalid) { // handle error. } }
|
在这个方法里,我们使用theForm.invalid
就可以获得这个表单是否验证成功的状态,也可以用’theForm.value’获得所有的表单数据。在这里,我们把表单数据打印到控制台来检查数据。至于如何从这个表单引用中获取控件数据和状态,会在接下来再讲。
使用ngModel绑定数据
接下来,我们需要绑定数据。假设我们的业务是打开这个页面的时候获取用户数据,然后显示到页面表单上。我们在组件的构造方法中创建一个模拟的用户数据:
1 2 3 4 5 6 7 8 9 10 11
|
export class TemplateFormsComponent { user: any; constructor() { this.user = { name: '张三', mobile: 13800138001, city: '北京', street: '朝阳望京...' }; } }
|
然后在模板中将这个组件中的数据绑定到模板页面上:
1 2 3 4 5
|
<input type="text" name="name" [(ngModel)]="user.name"> <input type="text" name="mobile" [ngModel]="user.mobile"> <input type="text" name="city" [ngModel]="user.city"> <input type="text" name="street" [ngModel]="user.street"> <!-- 其他的输入框都类似 -->
|
在这里,我们使用[(ngModel)]="user.name"
,这是双向绑定的方式,这样,当我们修改页面上的数据的时候,在组件中也能获得更新后的数据;同时,如果在组件中更新了数据,在页面上也能更新。
为了演示这个双向绑定跟单向绑定的区别,我们只对姓名使用双向绑定,对其他的都是用单向绑定,也就是[ngModel]="user.mobile"
。使用[]
的单向绑定是从模板到组件的绑定,也就是页面中的输入的数据改变,组件中的数据也会改变。但是组件中的数据更新不会引起页面上该数据的更新。
使用单向绑定可以减少数据的更新检查,从来可以提高性能。
如果不需要数据的初始化,我们其实可以只用ngModel
,例如:
1
|
<input type="text" name="city" ngModel>
|
这样,我们在组件中创建的用户数据就无法显示到页面上,但是,他还是能够将页面上输入的数据绑定到组件中的数据上。
在Angular2中,使用ngModel
结合name
属性来创建一个表单控件FormControls
。例如上面的<input name="city" ngModel>
就对应一个userForm
里面的控件city
。由于我们在提交方法里面将这个userForm
作为参数传到方法里,我们可以在方法里面获得所有的表单控件theForm.controls
,它是一个Map类型的对象,key是所有的表单元素的name
,值就是一个FormControl
对象,里面保存着数据、和验证结果、是否修改等状态。也正是因为这些FormControls
,我们才能够使用theForm.value
的方式获取表单里的数据。当我们点击保存按钮的时候,就能在日志里面看到表单的数据:
1 2 3 4 5 6
|
{ name: "张三", mobile: 13800138001, city: "北京", street: "朝阳望京..." }
|
使用ngModelGroup分组显示
一般情况下,我们的model数据有可能是嵌套的,比如对于用户信息来说,城市和街道可能在一个地址对象address
里,例如:
1 2 3 4 5 6 7 8
|
{ name: "张三", mobile: 13800138001, address: { city: "北京", street: "朝阳望京..." } }
|
对于这样的数据,我们就可以使用ngModelGroup来分组。模板就是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
<form #userForm="ngForm" (ngSubmit)="logForm(userForm)"> <label>姓名:</label> <input type="text" name="name" [(ngModel)]="user.name"> <label>电话:</label> <input type="text" name="mobile" [ngModel]="user.mobile"> <fieldset ngModelGroup="address"> <label>城市:</label> <input type="text" name="city" [ngModel]="user.address.city"> <label>街道:</label> <input type="text" name="street" [ngModel]="user.address.street"> <button type="submit">保存</button> </fieldset> </form>
|
这样我们就把地址信息都封装到一个address对象里面。注意我们绑定的数据的结构也发生改变,这样,我们也需要修改我们的组件里面的用户数据:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
export class TemplateFormsComponent { user: any; constructor() { this.user = { name: '张三', mobile: 13800138001, address: { city: '北京', street: '朝阳望京...' } }; } }
|
至此,我们的表单的基本功能就算完成了。我们在面板中创建了表单,在组件中初始化了用户数据,并显示到页面上,在页面上用ngModel,将页面上的数据更改绑定到组件上。同时,使用name
属性,使得表单里面的所有数据都成为FormControl
对象。在提交所调用的方法里,获得了表单的验证状态和数据。
表单控件的验证和状态
下一步,我们来添加数据验证,Angular2为我们提供了几种最基本的验证:
- required:表明该数据是必须的。
- minlength:设置该字段的长度的最小值,即使输入的是数字,也按照字符串来判断长度。
- maxlength:设置该字段的长度的最大值。
- pattern:使用正则表达式验证
在使用Angular的验证之前,我们首先需要关闭浏览器默认的验证,不然,如果某一个输入不合法,提交按钮就无法提交。我们在form
里添加novalidate
:
1
|
<form #userForm="ngForm" (ngSubmit)="logForm(userForm)" novalidate>
|
然后,我们对姓名输入框添加验证,并根据验证的结果显示不同的提示,同时,为了演示Angular2表单控件的特性,再添加几个提示,来显示该值的状态,代码如下:
1 2 3 4 5 6 7 8
|
<input type="text" name="name" [(ngModel)]="user.name" #name="ngModel" required minlength="3"> <span *ngIf="name.pristine" class="label label-primary">未修改</span> <span *ngIf="name.dirty" class="label label-warning">已修改</span> <span *ngIf="name.valid" class="label label-success">有效</span> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> <p *ngIf="name.errors?.minlength">姓名最小长度为3</p> <p *ngIf="name.errors?.required">必须输入姓名</p> </div>
|
首先,我们在input上添加了2个验证,required
和minlength="3"
。
其次,我们使用#name="ngModel"
创建了一个模板引用变量,这样我们在下面就可以使用name
来获取这个表单控件(FormControl
)的引用。表单控件有一些属性,如pristine
,
dirty
, valid
, touched
,这几个都是状态类型,表示某一种状态是否为真。除此以外还有控件的值可以用name.value
获取。最后,还有验证的错误信息结果,会放在name.errors
里。
在上面的代码里,我们用<span *ngIf="name.pristine" class="label label-primary">未修改</span>
,在控件值未被修改的时候,显示一个lebel。同样,在被修改、验证有效的时候显示相应的标签。
最后,所有的验证结果的错误信息会保存在name.errors
里,如果没有数据验证错误,这个errors值就是null
,所以,在上面的代码里,我们用name.errors?.minlength
,这表示,如果errors
不为null,而且errors.minlength
也不为空的时候,才显示里面的信息。
我们可以看到,表单控件的验证会将验证器的名字作为key放在errors
里面,对应的值是true
。我们就是用这个特性,来根据控件验证的不同结果,来显示友好的错误信息。
如果运行我们的实例,可以发现,对于姓名,如果清空它的值,发现只有一个错误信息,就是必须输入姓名
。你可能会觉得,这时候,值为空,那他的长度也小于3,那么minlength
这个错误也应该被检测到才对,但是实际上,遇到第一个错误以后,就没有其他的验证。
在上面姓名输入框上,我们使用#name="ngModel"
创建了一个模板引用变量,然后在接下来的模板里面使用它获得表单控件。实际上,我们也可以直接使用之前定义的对ngForm
的引用,来获得这个表单里所有控件的状态。例如,对电话,我们使用下面的方式:
1 2 3 4 5 6 7 8
|
<input type="text" name="mobile" [ngModel]="user.mobile" required minlength="11" maxlength="11"> <span *ngIf="userForm.controls.mobile?.pristine" class="label label-primary">未修改</span> <span *ngIf="userForm.controls.mobile?.dirty" class="label label-warning">已修改</span> <span *ngIf="userForm.controls.mobile?.valid" class="label label-success">有效</span> <div [hidden]="userForm.controls.mobile?.valid || userForm.controls.mobile?.pristine" class="alert alert-danger"> <p *ngIf="userForm.controls.mobile?.errors?.minlength">电话长度必须为11</p> <p *ngIf="userForm.controls.mobile?.errors?.required">必须输入电话</p> </div>
|
在这里,我们没有获取对mobile
的模板引用,而是用ngForm
的引用获得:
1
|
userForm.controls.mobile?.pristine
|
当获取验证错误结果时:
1
|
userForm.controls.mobile?.errors?.minlength
|
注意这里在mobile上就使用?
是因为,在使用ngIf
渲染页面上的元素的时候,这个表单控件还没有初始化完成,如果不加这个?
,就会出现错误。
根据验证状态定义样式
Angular的表单验证,除了在控件上的数据以外,它还会根据状态在控件所在的html元素上添加css样式:
所以,我们只需要定义相关的css,就可以实现根据状态显示不同的效果。
1 2 3 4 5 6
|
.ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } .ng-invalid:not(form).ng-invalid:not(fieldset) { border-left: 5px solid #a94442; /* red */ }
|
结合各种css的选择器,我们就可以根据表单控件的状态实现各种显示的样式。
在组件中获取表单控件数据
最后,我们再看看怎样在组件中获取这些控件的状态和结果,在上面,我们给ngForm
添加了一个提交方法:
1
|
<form #userForm="ngForm" (ngSubmit)="logForm(userForm)" novalidate>
|
然后在组件中,这个logForm(userForm)
方法如下:
1 2 3 4 5 6 7 8 9 10
|
logForm(theForm: NgForm) { if (theForm.invalid) { if (theForm.controls['name'].errors) { this.nameErrorMsg = 'name error:' + JSON.stringify(theForm.controls['name'].errors); } else { this.nameErrorMsg = null; } } console.log(theForm.value); }
|
在这个方法里,theForm
就是ngForm
的模板引用实例,类型是NgForm
的。
如果表单验证有失败,theForm.invalid
就是false。theForm.controls
就是这个表单里的所有控件,如果想获取姓名的验证结果,就是theForm.controls['name'].errors
。
用这种方式,我们就可以在组件中获取所有表单控件的数据、验证状态、错误信息等。
重置表单
一般情况下,如果是新建用户信息,我们需要在保存成功以后,清空当前数据,重置表单的状态,等待用户重新输入。如果我们只是清空数据,这时候那些验证错误就会被检测到,我们我们需要将表单控件也都重置成未修改状态。这在Angular2里很简单,它提供了一个reset
方法。
我们在
1
|
<button (click)="reset(userForm)">重置</button>
|
然后在组件里:
1 2 3 4
|
reset(theForm: NgForm) { theForm.reset(); return false; }
|
注意我们需要让这个方法返回false,这样他就不会触发submit的方法。
在官方的文档中,还提供了另一种技巧来实现这种重置,就是在form上使用ngIf
:
1
|
<form #userForm="ngForm" (ngSubmit)="logForm(userForm)" novalidate *ngIf="active">
|
只有在active
为true时这个表单才会创建。
然后在重置的时候,设置这个active
为false,这样这个表单就会被销毁,然后用setTimeout
的方式再设置它为true,这个表单就会重新创建,这样就实现了重置的效果。
1 2 3 4 5 6 7 8
|
reset() { this.user = { // 重置用户数据 address: {} }; this.active = false; setTimeout(() => this.active = true, 0); return false; }
|
这也是一种小窍门,可以在某些情况下使用。
总结
至此,有关模型驱动的表单的基本用法大致完成,再总结一下模型驱动的表单的基本特性:
- 所有的表单控件的定义都在模板里
- 所有的验证器都在模板里面添加
- 表单数据的状态、验证结果都在模板上通过判断表单里面控件数据的状态来显示
- 如果需要测试这部分的代码,需要使用e2e(端到端)测试,也就是在浏览器里面