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

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

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

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

实例

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

它是一个用户信息输入的表单,包括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样式:

所以,我们只需要定义相关的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(端到端)测试,也就是在浏览器里面

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

(0)

相关推荐

  • 详谈Angular 2+ 的表单(一)之模板驱动型表单

    摘要 在企业应用开发时,表单是一个躲不过去的事情,和面向消费者的应用不同,企业领域的开发中,表单的使用量是惊人的.这些表单的处理其实是一个挺复杂的事情,比如有的是涉及到多个 Tab 的表单,有的是向导形式多个步骤的,各种复杂的验证逻辑和时不时需要弹出的对话框等等.笔者试图在这一系列文章中对 Angular 中的表单处理做一个相对完整的梳理. Angular 中提供两种类型的表单处理机制,一种叫模版驱动型(Template Driven)的表单,另一种叫模型驱动型表单( Model Driven

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

    在网页开发中,表单估计是最常用的一个,同时也是最麻烦.最容易出问题的.在一个稍微复杂一点的应用中,我们除了用表单元素收集数据,还需要验证,几个数据之间可能还会相互关联,然后根据不同的数据值调用不同的业务逻辑等. 使用Angular提供的数据绑定的功能,我们可以很容易就在组件中获得用户输入的数据,Angular也提供了几种验证方式方便我们进行数据的校验.但是,一些自定义的数据验证.数据交互和业务逻辑还是需要自己处理. 在Angular2中,提供了2种表单实现方式,分别是'template-driv

  • 详解Angular2响应式表单

    本文将半翻译半总结的讲讲ng2官网的另一个未翻译高级教程页面. 文章目的是使用ng2提供的响应式表单技术快速搭出功能完善丰富的界面表单组件. 响应式表单是一项响应式风格的ng2技术,本文将解释响应式表单并用来创建一个英雄详情编辑器. 包含内容: 响应式表单介绍 开始搭建 创建数据模型 创建响应式的表单组件 创建组建的模板文件 引入ReactiveFormsModule 显示HeroDetailComponent 添加一个FormGroup 看看表单模型 介绍FormBuilder 验证的需求 放

  • 详解使用Mybatis-plus + velocity模板生成自定义的代码

    pom.xml文件的配置 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>com.baomidou</groupId&

  • 详解C++11中模板的优化问题

    1. 模板的右尖括号 在泛型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(>>)会被编译器解析成右移操作符,而不是模板参数表的结束.我们先来看一段关于容器遍历的代码,在创建的类模板 Base 中提供了遍历容器的操作函数 traversal(): // test.cpp #include <iostream> #include <vector> using namespace std; template <typename T> class

  • 详解JAVA设计模式之模板模式

    在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板.它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行.这种类型的设计模式属于行为型模式. 介绍 意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中.模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤. 主要解决:一些方法通用,却在每一个子类都重新写了这一方法. 何时使用:有一些通用的方法. 如何解决:将这些通用算法抽象出来. 关键代码:在抽象类实现,其他步骤在子

  • 详解C++11 变参模板

    1.概述 变参模板(variadic template)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数.任意类型的参数.相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进.然而由于可变模版参数比较抽象,使用起来需要一定的技巧,掌握也存在一定的难度. 2.可变模版参数的展开 可变模板参数和普通模板参数的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号"-"

  • 详解C语言快速排序三种方法的单趟实现

    目录 交换排序的思想 冒泡排序的思想 快速排序的整体框架 快速排序单趟实现逻辑 1. hoare版本单趟实现(左右指针法) 2.挖坑法单趟排序实现 3.前后指针法 交换排序的思想 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动. 冒泡排序的思想 冒泡排序比较简单,我们之前使用也很多,简单讲解,就是比较两个数,如果比他大就交换,没有他大就接着向后比,一直到数组结束,这是单趟

  • 详解C++中函数模板的定义与使用

    目录 1. 前言 2. 初识函数模板 2.1 语法 2.2 实例化 2.3 实参推导 3. 重载函数模板 1. 前言 什么是函数模板? 理解什么是函数模板,须先搞清楚为什么需要函数模板. 如果现在有一个需求,要求编写一个求 2 个数字中最小数字的函数,这 2 个数字可以是 int类型,可以是 float 类型,可以是所有可以进行比较的数据类型…… 常规编写方案:针对不同的数据类型编写不同的函数. #include <iostream> using namespace std; //针对 int

  • 详解小程序之简单登录注册表单验证

    这段时间在做员工管理的小程序,前期在登录注册上花了不少功夫,今天就给大家分享下. 效果图,wxss的内容较简单,自己编写即可. ##主要内容 一.首先我是在util.js中引入表单正则验证规则,给予login.js来引用 function regexConfig() { var reg = { userid: /^[A-Za-z0-9]+$/, //邮箱正则验证 phone: /^1(3|4|5|7|8)\d{9}$/, //手机号正则验证 cards: /^[\u4e00-\u9fa5]{2,

随机推荐