详解Angular2表单-模板驱动的表单(Template-Driven Forms)


Posted in Javascript onAugust 04, 2017

在网页开发中,表单估计是最常用的一个,同时也是最麻烦、最容易出问题的。在一个稍微复杂一点的应用中,我们除了用表单元素收集数据,还需要验证,几个数据之间可能还会相互关联,然后根据不同的数据值调用不同的业务逻辑等。

使用Angular提供的数据绑定的功能,我们可以很容易就在组件中获得用户输入的数据,Angular也提供了几种验证方式方便我们进行数据的校验。但是,一些自定义的数据验证、数据交互和业务逻辑还是需要自己处理。

在Angular2中,提供了2种表单实现方式,分别是'template-driven'(模板驱动的表单)和'model-driven'(模型驱动表单)。在这篇文章中,我们先来看看模板驱动的表单。顾名思义,模板驱动的表单就是大部分表单相关代码都在模板里,通过在模板里面添加ngForm, ngModel和ngModelGroup等属性来定义模板和验证信息,以及它跟组件之间的数据交互。

实例

下图是这篇文章使用的实例的界面:

详解Angular2表单-模板驱动的表单(Template-Driven Forms)

它是一个用户信息输入的表单,包括4个字段,用户名、电话、城市和街道,演示了如何使用表单,给各个字段添加验证并显示验证结果,以及如何在组件中判断是否出错并获取出错信息。

项目源码可以从github获取,这个项目包含了几个Angular2表单相关的实例,可以使用下面的命令获取本文所对应的代码:

git clone https://github.com/Mavlarn/angular2-forms-tutorial

然后进入项目目录,运行下面的命令安装依赖然后运行测试服务器:

cd angular2-forms-tutorial
git checkout template-driven # 检出该文所使用的tag
npm install
npm start

该项目是基于之前的Angular2-basic模板,这个教程相关的代码都在'template-forms'目录里面。

引入FormsModule

首先,我们需要在app.module.ts里引入FormsModule。

import { FormsModule } from '@angular/forms';
//省略其他
@NgModule({
 imports: [ BrowserModule, FormsModule ],
 //省略其他
})

初始表单

然后,我们从一个基本的html表单开始:

<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中使用模板驱动的表单。如果想查看完整的带样式的代码,请查看源文件。

<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里,都提供了一个模板引用变量的功能,通过#加变量实现。通过这个功能,我们可以在同一元素、兄弟元素或任何子元素中引用模板引用变量。这样听着还是不好理解,我们看一个例子:

<input #phone placeholder="phone number">
<button (click)="callPhone(phone.value)">Call</button>

在这个例子中,我们通过#phone定义了一个变量,它所指的就是这个input元素,phone.value也就是这个输入框输入的值。

除了使用#,也可以使用ref-,例如ref-phone形式的定义跟#phone是一样的。

我们可以对任何的DOM元素使用这种方式获取当前引用,也可以对任何的Angular2的指令使用。在这个表单的例子中,我们这样来获取这个ngFrom的引用:

<form #userForm="ngForm">

其中'ngForm'就是当前这个指令,这样在这个模板里面,我们可以用userForm获得表单的所有数据。

提交表单

在html中,我们要提交一个form,会在form里写一个action的属性,然后,用一个类型为'submit'的按钮来提交。但是,在Angular2中,我们需要使用ngSubmit事件:

<form #userForm="ngForm" (ngSubmit)="logForm(userForm)">
  <button type="submit">保存</button>
</form>

这样,当用户点击保存按钮的时候,Angular2会使用自己的验证机制,验证所有的数据,然后在调用'logForm(userForm)'方法。

在我们的组件中,实现这个方法:

logForm(theForm: NgForm) {
  console.log(theForm.value);
  if (theForm.invalid) {
    // handle error.
  }
}

在这个方法里,我们使用theForm.invalid就可以获得这个表单是否验证成功的状态,也可以用'theForm.value'获得所有的表单数据。在这里,我们把表单数据打印到控制台来检查数据。至于如何从这个表单引用中获取控件数据和状态,会在接下来再讲。

使用ngModel绑定数据

接下来,我们需要绑定数据。假设我们的业务是打开这个页面的时候获取用户数据,然后显示到页面表单上。我们在组件的构造方法中创建一个模拟的用户数据:

export class TemplateFormsComponent {
  user: any;
  constructor() {
    this.user = {
      name: '张三',
      mobile: 13800138001,
      city: '北京',
      street: '朝阳望京...'
    };
  }
}

然后在模板中将这个组件中的数据绑定到模板页面上:

<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,例如:

<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的方式获取表单里的数据。当我们点击保存按钮的时候,就能在日志里面看到表单的数据:

{
  name: "张三",
  mobile: 13800138001,
  city: "北京",
  street: "朝阳望京..."
}

使用ngModelGroup分组显示

一般情况下,我们的model数据有可能是嵌套的,比如对于用户信息来说,城市和街道可能在一个地址对象address里,例如:

{
  name: "张三",
  mobile: 13800138001,
  address: {
    city: "北京",
    street: "朝阳望京..."
  }
}

对于这样的数据,我们就可以使用ngModelGroup来分组。模板就是这样:

<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对象里面。注意我们绑定的数据的结构也发生改变,这样,我们也需要修改我们的组件里面的用户数据:

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:

<form #userForm="ngForm" (ngSubmit)="logForm(userForm)" novalidate>

然后,我们对姓名输入框添加验证,并根据验证的结果显示不同的提示,同时,为了演示Angular2表单控件的特性,再添加几个提示,来显示该值的状态,代码如下:

<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的引用,来获得这个表单里所有控件的状态。例如,对电话,我们使用下面的方式:

<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的引用获得:

userForm.controls.mobile?.pristine

当获取验证错误结果时:

userForm.controls.mobile?.errors?.minlength

注意这里在mobile上就使用?是因为,在使用ngIf渲染页面上的元素的时候,这个表单控件还没有初始化完成,如果不加这个?,就会出现错误。

根据验证状态定义样式

Angular的表单验证,除了在控件上的数据以外,它还会根据状态在控件所在的html元素上添加css样式:

详解Angular2表单-模板驱动的表单(Template-Driven Forms)

所以,我们只需要定义相关的css,就可以实现根据状态显示不同的效果。

.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添加了一个提交方法:

<form #userForm="ngForm" (ngSubmit)="logForm(userForm)" novalidate>

然后在组件中,这个logForm(userForm)方法如下:

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方法。

我们在里面添加一个重置按钮:

<button (click)="reset(userForm)">重置</button>

然后在组件里:

reset(theForm: NgForm) {
  theForm.reset();
  return false;
}

注意我们需要让这个方法返回false,这样他就不会触发submit的方法。

在官方的文档中,还提供了另一种技巧来实现这种重置,就是在form上使用ngIf:

<form #userForm="ngForm" (ngSubmit)="logForm(userForm)" novalidate *ngIf="active">

只有在active为true时这个表单才会创建。

然后在重置的时候,设置这个active为false,这样这个表单就会被销毁,然后用setTimeout的方式再设置它为true,这个表单就会重新创建,这样就实现了重置的效果。

reset() {
  this.user = { // 重置用户数据
    address: {}
  };
  this.active = false;
  setTimeout(() => this.active = true, 0);
  return false;
}

这也是一种小窍门,可以在某些情况下使用。

总结

至此,有关模板驱动的表单的基本用法大致完成,再总结一下模板驱动的表单的基本特性:

  1. 所有的表单控件的定义都在模板里
  2. 所有的验证器都在模板里面添加
  3. 表单数据的状态、验证结果都在模板上通过判断表单里面控件数据的状态来显示
  4. 如果需要测试这部分的代码,需要使用e2e(端到端)测试,也就是在浏览器里面

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
实用javaScript技术-屏蔽类
Aug 15 Javascript
弹出层之1:JQuery.Boxy (一) 使用介绍
Oct 06 Javascript
捕获浏览器关闭、刷新事件不同情况下的处理方法
Jun 02 Javascript
jQuery .attr()和.removeAttr()方法操作元素属性示例
Jul 16 Javascript
15个jquery常用方法、小技巧分享
Jan 13 Javascript
极力推荐10个短小实用的JavaScript代码段
Aug 03 Javascript
gulp教程_从入门到项目中快速上手使用方法
Sep 14 Javascript
浅谈 Vue 项目优化的方法
Dec 16 Javascript
vue.js 底部导航栏 一级路由显示 子路由不显示的解决方法
Mar 09 Javascript
Vue中对比scoped css和css module的区别
May 17 Javascript
js实现网页同时进行多个倒计时功能
Feb 25 Javascript
在nuxt中使用路由重定向的实例
Nov 06 Javascript
微信小程序 本地图片按照屏幕尺寸处理
Aug 04 #Javascript
Javascript实现跨域后台设置拦截的方法详解
Aug 04 #Javascript
微信小程序 五星评分的实现实例
Aug 04 #Javascript
JavaScript中in和hasOwnProperty区别详解
Aug 04 #Javascript
JavaScript hasOwnProperty() 函数实例详解
Aug 04 #Javascript
微信小程序 websocket 实现SpringMVC+Spring+Mybatis
Aug 04 #Javascript
Angular实现响应式表单
Aug 04 #Javascript
You might like
PHP截取汉字乱码问题解决方法mb_substr函数的应用
2008/03/30 PHP
PHP检测字符串是否为UTF8编码的常用方法
2014/11/21 PHP
php获取错误信息的方法
2015/07/17 PHP
PHP判断手机是IOS还是Android
2015/12/09 PHP
SUN的《AJAX与J2EE》全文译了
2007/02/23 Javascript
jquery 触发a链接点击事件解决方案
2013/05/02 Javascript
JS和JQUERY获取页面大小,滚动条位置,元素位置(示例代码)
2013/12/14 Javascript
jquery JSON的解析方式示例介绍
2014/07/27 Javascript
javascript判断并获取注册表中可信任站点的方法
2015/06/01 Javascript
jquery实现华丽的可折角广告代码
2015/09/02 Javascript
基于jQuery实现简单的折叠菜单效果
2015/11/23 Javascript
原生javascript实现的ajax异步封装功能示例
2016/11/03 Javascript
微信小程序之MaterialDesign--input组件详解
2017/02/15 Javascript
Webpack框架核心概念(知识点整理)
2017/12/22 Javascript
浅谈Vue数据响应
2018/11/05 Javascript
js 根据对象数组中的属性进行排序实现代码
2019/09/12 Javascript
微信小程序swiper实现文字纵向轮播提示效果
2020/01/21 Javascript
《javascript设计模式》学习笔记四:Javascript面向对象程序设计链式调用实例分析
2020/04/07 Javascript
python实现可将字符转换成大写的tcp服务器实例
2015/04/29 Python
python实现矩阵乘法的方法
2015/06/28 Python
使用Python实现租车计费系统的两种方法
2018/09/29 Python
对python3新增的byte类型详解
2018/12/04 Python
在python中对变量判断是否为None的三种方法总结
2019/01/23 Python
简单了解python关系(比较)运算符
2019/07/08 Python
多版本python的pip 升级后, pip2 pip3 与python版本失配解决方法
2019/09/11 Python
如何修复使用 Python ORM 工具 SQLAlchemy 时的常见陷阱
2019/11/19 Python
Python统计文本词汇出现次数的实例代码
2020/02/27 Python
在tensorflow以及keras安装目录查询操作(windows下)
2020/06/19 Python
I.T中国官网:精选时尚设计师单品网购平台
2018/03/26 全球购物
小学开学寄语
2014/01/19 职场文书
债务授权委托书范本
2014/10/17 职场文书
合伙经营协议书范本(通用版)
2014/12/03 职场文书
婚宴主持词
2015/06/30 职场文书
教师节感想
2015/08/11 职场文书
《游戏王:大师决斗》新活动上线 若无符合卡组可免费租用
2022/04/13 其他游戏
Python如何将list中的string转换为int
2022/07/15 Ruby