微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

摘要

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

Angular 中提供两种类型的表单处理机制,一种叫模版驱动型(Template Driven)的表单,另一种叫模型驱动型表单( Model Driven ),这后一种也叫响应式表单 ( Reactive Forms ),由于模版驱动中有一个 ngModel 的指令,容易和这里说的模型驱动混淆,所以在我们的文章中叫后一种说法:响应式表单。

第一篇主要介绍模版驱动型的表单。

号外

本文评论区会抽出5位童鞋,赠送笔者的 《Angular 从零到一》纸书,机不可失,大家踊跃发言哦。

模版驱动的表单

模版驱动的表单和 AngularJS 对于表单的处理类似,把一些指令(比如 ngModel )、数据值和行为约束(比如 require 、 minlength 等等)绑定到模版中(模版就是组件元数据 @Component 中定义的那个 template ),这也是模版驱动这个叫法的来源。总体来说,这种类型的表单通过绑定把很多工作交给了模版。

模版驱动的例子

还是用例子来说话,比如我们有一个用户注册的表单,用户名就是 email ,还需要填的信息有:住址、密码和重复密码。这个应该是比较常见的一个注册时需要的信息了。那么我们第一步来建立领域模型:

rush:js;"> // src/app/domain/index.ts export interface User { // 新的用户id一般由服务器自动生成,所以可以为空,用 ? 标示 id?: string; email: string; password: string; repeat: string; address: Address; } export interface Address { province: string; // 省份 city: string; // 城市 area: string; // 区县 addr: string; // 详细地址 }

接下来我们建立模版文件一个最简单的 HTML 模版,先不增加任何的绑定或事件处理:

rush:js;">

渲染之后的效果就像下面这样:

简单的Form

数据绑定

对于模版驱动型的表单处理,我们首先需要在对应的模块中引入 FormsModule ,这一点千万不要忘记了。

rush:js;"> import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from "@angular/forms"; import { TemplateDrivenComponent } from './template-driven/template-driven.component'; @NgModule({ imports: [ CommonModule,FormsModule ],exports: [TemplateDrivenComponent],declarations: [TemplateDrivenComponent] }) export class FormDemoModule { } 进行模版驱动类型的表单处理的一个必要步骤就是建立数据的双向绑定,那么我们需要在组件中建立一个类型为 User 的成员变量并赋初始值。 // template-driven.component.ts // 省略元数据和导入的类库信息 export class TemplateDrivenComponent implements OnInit { user: User = { email: '',password: '',repeat: '',address: { province: '',city: '',area: '',addr: '' } }; // 省略其他部分 }

有了这样一个成员变量之后,我们在组件模版中就可以使用 ngModel 进行绑定了。

令人困惑的 ngModel

我们在 Angular 中可以使用三种形式的 ngModel 表达式: ngModel,[ngModel] 和 [(ngModel)] 。但无论那种形式,如果你要使用 ngModel 就必须为该控件(比如下面的 input )指定一个 name 属性,如果你忘记添加 name 的话,多半你会看到下面这样的错误

ERROR Error: Uncaught (in promise): Error: If ngModel is used within a form tag,either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.

ngModel 和 FormControl

假如我们使用的是 ngModel ,没有任何中括号小括号的话,这代表着我们创建了一个 FormControl 的实例,这个实例将会跟踪值的变化、用户的交互、验证状态以及保持视图和领域对象的同步等工作。

rush:js;">

如果我们将这个控件放在一个 Form 表单中, ngModel 会自动将这个 FormControl 注册为 Form 的子控件。下面的例子中我们在

加上了 ngForm 指令,声明这是一个 Angular 可识别的表单,而 ngModel 会将 注册成表单的子控件,这个子控件的名字就是 email ,而且 ngModel 会基于这个子控件的值去绑定表单的的值,这也是为什么需要显式声明 name 的原因。

其实在我们导入 FormsModule 的时候,所有的 标签都会认的被认为是一个 NgForm ,因此我们并不需要显式的在标签中写 ngForm 这个指令。

rush:js;">

这一切现在都是不可见的,所以大家可能还是有些困惑,那么下面我们将其“可视化”,这需要我们引用一下表单对象,所以我们使用 #f="ngForm" 以便我们可以在模版中输出表单的一些特性。

rush:js;">
ovalidate #f="ngForm"> ...
{{f.value | json}}

这时如果我们在 email 中输入 sss ,可以看到下图的以 JSON 形式出现的表单值:

控件的输入值同步到了表单的值中

单向数据绑定

那么接下来,我们看看 [ngModel] 有什么用?如果我们想给控件设置一个初始值怎么办呢,这时就需要进行一个单向绑定,方向是从组件到视图。我们可以做的是在初始化 User 的时候,将 email 属性设置成

而且在模版中使用 [ngModel]="user.email" 进行单向绑定,这个语法其实和普通的属性绑定是一样的,用中括号标示这是一个要进行数据绑定的属性,等号右边是需要绑定的值(这里是 user.email )。那么我们就可以得到下面这样的输出了, email 的初始值被绑定成功!

单向数据绑定

双向数据绑定

但上面的例子存在一个问题,数据的绑定是单向的,也就是说,在输入框进行输入的时候,我们的 user 的值不会随之改变的。为了更好的说明,我们将 user 和 表单的值同时输出

user: {{user | json}}
表单: {{f.value | json}}

此时我们将默认的电子邮件改成 的话,表单的值是改变了,但 user 并未改变。

输入的值影响了表单,但不会影响领域对象

如果我们希望的是在输入时,这个输入的值也反向的影响我们的 user 对象的值的话,那就需要用到双向绑定了,也就是 [(ngModel)] 需要上场了。

表单和领域对象的值保持了同步

无论如何,这个 [()] 表达真是很奇怪的样子,其实这个表达是一个语法糖。只要我们知道下面的两种写法是等价的,我们就会很清楚的理解了:用这个语法糖你就不用既写数据绑定又写事件绑定了。

rush:js;">

ngModelGroup 是什么鬼?

如果我们仔细观察上面的输出的话,会发现一个问题: user 中是有一个嵌套对象 address 的,而表单中没有嵌套对象的。如果要实现表单中的结构和领域对象的结构一致的话,我们就得请出 ngModelGroup 了。 ngModelGroup 会创建并绑定一个 FormGroup 到该 DOM 元素。 FormGroup 又是什么呢?简单来说,是一组 FormControl。

rush:js;">
省份

这样的话,我们再来看一下输出,现在就完全一致了:

表单和领域对象的结构也完全一致了

数据验证

模版驱动型的表单的验证也是主要由模版来处理的,在看怎么使用之前,需要界定一下验证规则:

  • 三个必填项: email,password 和 repeat
  • email 的形式需要符合电子邮件的标准
  • password 和 repeat 必须一致

当然除了这几个规则,我们还希望在表单未验证通过时提交按钮是不可用的。

rush:js;">
ovalidate #f="ngForm">

Angular 中有几种内建支持的验证器( Validators )

  • required - 需要 FormControl 有非空值
  • minlength - 需要 FormControl 有最小长度的值
  • maxlength - 需要 FormControl 有最大长度的值
  • pattern - 需要 FormControl 的值可以匹配正则表达式

如果我们想看到结果的话,我们可以在模版中加上下面的代码,将错误以 JSON 形式输出即可。

rush:js;">
email 验证: {{f.controls.email?.errors | json}}

我们看到,如果不填电子邮件的话,错误的 JSON 是 {"required": true} ,这告诉我们目前有一个 required 的规则没有被满足。

验证结果

当我们输入一个字母 w 之后,就会发现错误变成了下面的样子。这是因为我们对于 email 应用了多个规则,当必填项满足后,系统会继续检查其他验证结果。

rush:js;"> { "pattern": { "requiredPattern": "^([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}$","actualValue": "w" } }

通过几次实验,我们应该可以得出结论,当验证未通过时,验证器返回的是一个对象, key 为验证的规则(比如 required,minlength 等),value 为验证结果。如果验证通过,返回的是一个 null 。

知道这一点后,我们其实就可以做出验证出错的提示了,为了方便引用,我们还是导出 ngModel 到一个 email 引用,然后就可以访问这个 FormControl 的各个属性了:验证的状态( valid/invalid )、控件的状态(是否获得过焦点 -- touched/untouched,是否更改过内容 -- pristine/dirty 等)

rush:js;">

相关推荐