摘要
在企业应用开发时,表单是一个躲不过去的事情,和面向消费者的应用不同,企业领域的开发中,表单的使用量是惊人的。这些表单的处理其实是一个挺复杂的事情,比如有的是涉及到多个 Tab 的表单,有的是向导形式多个步骤的,各种复杂的验证逻辑和时不时需要弹出的对话框等等。笔者试图在这一系列文章中对 Angular 中的表单处理做一个相对完整的梳理。
Angular 中提供两种类型的表单处理机制,一种叫模版驱动型(Template Driven)的表单,另一种叫模型驱动型表单( Model Driven ),这后一种也叫响应式表单 ( Reactive Forms ),由于模版驱动中有一个 ngModel 的指令,容易和这里说的模型驱动混淆,所以在我们的文章中叫后一种说法:响应式表单。
第一篇主要介绍模版驱动型的表单。
号外
本文评论区会抽出5位童鞋,赠送笔者的 《Angular 从零到一》纸书,机不可失,大家踊跃发言哦。
模版驱动的表单
模版驱动的表单和 AngularJS 对于表单的处理类似,把一些指令(比如 ngModel )、数据值和行为约束(比如 require 、 minlength 等等)绑定到模版中(模版就是组件元数据 @Component 中定义的那个 template ),这也是模版驱动这个叫法的来源。总体来说,这种类型的表单通过绑定把很多工作交给了模版。
模版驱动的例子
还是用例子来说话,比如我们有一个用户注册的表单,用户名就是 email ,还需要填的信息有:住址、密码和重复密码。这个应该是比较常见的一个注册时需要的信息了。那么我们第一步来建立领域模型:
// 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; // 详细地址 }</div>
接下来我们建立模版文件,一个最简单的 HTML 模版,先不增加任何的绑定或事件处理:
<!-- template-driven.component.html --> <form novalidate> <label> <span>电子邮件地址</span> <input type="text" name="email" placeholder="请输入您的 email 地址"> </label> <div> <label> <span>密码</span> <input type="password" name="password" placeholder="请输入您的密码"> </label> <label> <span>确认密码</span> <input type="password" name="repeat" placeholder="请再次输入密码"> </label> </div> <div > <label> <span>省份</span> <select name="province"> <option value="">请选择省份</option> </select> </label> <label> <span>城市</span> <select name="city"> <option value="">请选择城市</option> </select> </label> <label> <span>区县</span> <select name="area"> <option value="">请选择区县</option> </select> </label> <label> <span>地址</span> <input type="text" name="addr"> </label> </div> <button type="submit">注册</button> </form></div>
渲染之后的效果就像下面这样:
简单的Form
数据绑定
对于模版驱动型的表单处理,我们首先需要在对应的模块中引入 FormsModule ,这一点千万不要忘记了。
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: '' } }; // 省略其他部分 }</div>
有了这样一个成员变量之后,我们在组件模版中就可以使用 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 的实例,这个实例将会跟踪值的变化、用户的交互、验证状态以及保持视图和领域对象的同步等工作。
<input type="text" name="email" placeholder="请输入您的 email 地址" ngModel></div>
如果我们将这个控件放在一个 Form 表单中, ngModel 会自动将这个 FormControl 注册为 Form 的子控件。下面的例子中我们在 <form> 中加上了 ngForm 指令,声明这是一个 Angular 可识别的表单,而 ngModel 会将 <input> 注册成表单的子控件,这个子控件的名字就是 email ,而且 ngModel 会基于这个子控件的值去绑定表单的的值,这也是为什么需要显式声明 name 的原因。
其实在我们导入 FormsModule 的时候,所有的 <form> 标签都会默认的被认为是一个 NgForm ,因此我们并不需要显式的在标签中写 ngForm 这个指令。
<!-- ngForm 并不需要显示声明,任何 <form> 标签默认都是 ngForm --> <form novalidate ngForm> <input type="text" name="email" placeholder="请输入您的 email 地址" ngModel> </form></div>
这一切现在都是不可见的,所以大家可能还是有些困惑,那么下面我们将其“可视化”,这需要我们引用一下表单对象,所以我们使用 #f="ngForm" 以便我们可以在模版中输出表单的一些特性。
<!-- 使用 # 把表单对象导出到 f 这个可引用变量中 --> <form novalidate #f="ngForm"> ... </form> <!-- 将表单的值以 JSON 形式输出 --> {{f.value | json}}</div>
这时如果我们在 email 中输入 sss ,可以看到下图的以 JSON 形式出现的表单值:
控件的输入值同步到了表单的值中
单向数据绑定
那么接下来,我们看看 [ngModel] 有什么用?如果我们想给控件设置一个初始值怎么办呢,这时就需要进行一个单向绑定,方向是从组件到视图。我们可以做的是在初始化 User 的时候,将 email 属性设置成 wang@163.com
user: User = { email: 'wang@163.com', ... };</div>
而且在模版中使用 [ngModel]="user.email" 进行单向绑定,这个语法其实和普通的属性绑定是一样的,用中括号标示这是一个要进行数据绑定的属性,等号右边是需要绑定的值(这里是 user.email )。那么我们就可以得到下面这样的输出了, email 的初始值被绑定成功!
单向数据绑定<