详解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 相关文章推荐
js判断运行jsp页面的浏览器类型以及版本示例
Oct 30 Javascript
php读取sqlite数据库入门实例代码
Jun 25 Javascript
原生js结合html5制作小飞龙的简易跳球
Mar 30 Javascript
微信小程序 滚动到某个位置添加class效果实现代码
Apr 19 Javascript
基于AngularJS实现表单验证功能
Jul 28 Javascript
微信小程序实现swiper切换卡内嵌滚动条不显示的方法示例
Dec 20 Javascript
详解vue 命名视图
Aug 14 Javascript
layer弹出层显示在top顶层的方法
Sep 11 Javascript
jquery实现垂直手风琴菜单
Mar 04 jQuery
vue+Element中table表格实现可编辑(select下拉框)
May 21 Javascript
Vue使用axios引起的后台session不同操作
Aug 14 Javascript
JavaScript实现京东快递单号查询
Nov 30 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中使用ExcelFileParser处理excel获得数据(可作批量导入到数据库使用)
2010/08/21 PHP
Destoon实现多表查询示例
2014/08/21 PHP
PHP GD库生成图像的几个函数总结
2014/11/19 PHP
PHP操作MySQL的mysql_fetch_* 函数的常见用法教程
2015/12/25 PHP
详解PHP中foreach的用法和实例
2016/10/25 PHP
Nginx+php配置文件及原理解析
2020/12/09 PHP
JavaScript CSS菜单功能 改进版
2008/12/20 Javascript
提取字符串中年月日的函数代码
2013/11/05 Javascript
Javascript中3个需要注意的运算符
2015/04/02 Javascript
原生JS实现旋转木马式图片轮播插件
2016/04/25 Javascript
JS+HTML5手机开发之滚动和惯性缓动实现方法分析
2016/06/12 Javascript
jquery select2的使用心得(推荐)
2016/12/04 Javascript
如何提高数据访问速度
2016/12/26 Javascript
JS百度地图搜索悬浮窗功能
2017/01/12 Javascript
vue树形结构获取键值的方法示例
2018/06/21 Javascript
vue中keep-alive组件的入门使用教程
2019/06/06 Javascript
layui实现鼠标移动到单元格上显示数据的方法
2019/09/11 Javascript
vue.js 实现a标签href里添加参数
2019/11/12 Javascript
Javascript 类型转换、封闭函数及常见内置对象操作示例
2019/11/15 Javascript
Python cx_freeze打包工具处理问题思路及解决办法
2016/02/13 Python
Python搭建APNS苹果推送通知推送服务的相关模块使用指南
2016/06/02 Python
Python实现的三层BP神经网络算法示例
2018/02/07 Python
python 遍历列表提取下标和值的实例
2018/12/25 Python
利用python实现简易版的贪吃蛇游戏(面向python小白)
2018/12/30 Python
Python图像的增强处理操作示例【基于ImageEnhance类】
2019/01/03 Python
python实现字符串加密 生成唯一固定长度字符串
2019/03/22 Python
Pandas DataFrame数据的更改、插入新增的列和行的方法
2019/06/25 Python
python Gunicorn服务器使用方法详解
2019/07/22 Python
pycharm如何实现跨目录调用文件
2020/02/28 Python
QML实现钟表效果
2020/06/02 Python
Python如何读写二进制数组数据
2020/08/01 Python
css3一款3D字体带阴影效果的实现步骤
2013/03/20 HTML / CSS
德国孕妇装和婴童服装网上商店:bellybutton
2018/04/12 全球购物
2014年应急工作总结
2014/12/11 职场文书
2019年员工旷工保证书!
2019/06/28 职场文书
VMware虚拟机安装 Windows Server 2022的详细图文教程
2022/09/23 Servers