Angular 为表单处理提供了丰富的支持,超越了常规的数据绑定,将表单处理作为顶级特性进行了专门的功能设计和开发。
HTML 表单
HTML 提供了表单基本功能:例如显示表单项、校验用户输入、提交表单数据等。但 HTML 表单无法很好地支持真实的业务,我们希望有更好的工具,帮助用户输入的数据、自定义校验规则、显示用户友好信息以及选择数据提交到服务器的方式等。
只使用浏览器标准功能的 HTML 表单:
1 | <form action="/regist" method="post"> |
注:不要启动 Angular2 服务器,直接打开本地文件。
由于没有加入任何校验,当我们点击“注册”按钮时,表单数据会被 HTTP POST 请求提交到服务器。
而我们希望:
- 每一个输入字段独立校验规则
- 如果用户不符合校验规则,显示提示信息
- 依赖的字段一起校验
- 应用提供提交到服务器的值,当用户点击注册按钮时,应该调用事件处理方法,并将表单的值传给这个事件处理方法。方法可以在表单发送数据之前,校验合法性或改变数据格式。
- 应用应该可以控制数据如何被发送到数据库,可以是一个 HTTP 请求,也可以是 Ajax 请求,也可以是 websocket 消息。
HTML可以部分满足前两个需求,例如 required
、 maxlength
、pattern
等:
1 | <div>用户名:<input type="text" required patter="[a-zA-Z0-9]+"></div> |
但会存在一些问题:
- 消息过于模糊
- 每隔一小段时间或输入框失去焦点时,消息消失
- 消息的样式无法自定义
定义正确的输入类型可以提供更好的用户体验,例如:
1 | <div>邮政编码:<input type="number" min="100000" max="999999"></div> |
但这样的写法仍然存在问题,并不一定每个 100000-999999 区间的数字都是邮政编码。然而真实场景中,我们可能会有更复杂的需求,例如河北省的用户只能输入河北的邮政编码等。
Angular 表单
Angular 提供了两种表单处理的方式,一种是模版的处理方式,另一种是响应式编程的方式,这两种方式表现为不同的 API。
1、模版式表单
表单的数据模型是通过组件模版中的相关指令来定义的,因为使用这种方式定义表单的数据模型时会受限于 HTML 的语法,所以,模版驱动方式只适合于一些简单的场景。
2、响应式表单
响应式表单是通过编写 TypeScript 代码而不是 HTML 的代码来创建一个底层的数据模型,在这个模型定义好后使用一些特定的指令,将模版上的 HTML 元素与底层的数据模型连接在一起。
不管是哪种表单,都有一个对应的数据模型来存储表单的数据。在模版式表单中,数据模型是由 Angular 给予你组件模版中的指令隐式创建的,而在
响应式表单中,你通过编码明确地创建数据模型然后将模版上的 HTML 元素与底层的数据模型连接在一起。
数据模型并不是一个任意的对象,它是由 angular/forms 模块中的一些特定的类,如 FormContol, FormGroup, FormArray 等组成的。在模版式表单中,你是不能直接访问到这些类的。
响应式表单并不会替你生成 HTML,模板仍然需要你自己来编写。
无论哪种表单,都需要引入相应的 Module:
模版式表单:FormsModule
响应式表单:ReactiveFormsModule
模版式表单
表单的数据模型只能通过组件模版中的相关「指令」来定义,这些指令包括 NgForm
、NgModel
、NgModelGroup
,这些指令均来自于 FormsModule
。
NgForm
指令代表整个表单,会被自动添加到每一个 <form>
标签上,并隐式地创建一个 FormGroup
类的实例,这个类代表表单的数据模型,并且存储数据。
如何自动添加到 <form>
标签上呢?
1 | ng g component templateForm |
template-form.component.html
1 | <form action="/createUser" method="post"> |
与本地打开会被 post 提交不同,开启 Angular 服务后,点击“注册”按钮是没有任何反应的,因为 Angular 接管了相关的操作。
NgForm 指令
NgForm
指令会自动创建一个 NgGroup
对象,它的子元素中含有 NgModel
指令的值会被添加到表单数据模型中:
NgForm
标签可以在<form>
标签以外使用,例如:<div ngForm>
,这与创建一个<form>
表单的效果是完全相同的。如果不希望用 Angular 自动处理表单,可以<form ngNoForm>
,这样 angular 不再接管相关操作。- 通过创建模板本地变量引用,
NgForm
表单可以被引用。
1 | <form #myForm="ngForm" action="/createUser" method="post"> |
声明了一个 myForm
模板变量,通过它访问 ngForm
对象的属性,例如 value
,value
属性是一个 js 对象,保存当前表单所有对象的值。
如果你希望把每个字段显示在页面上,可以将这个对象传给 Angular 的 json 管道。
1 | <div> |
然后我们在页面中输入一些内容,会发现并没有同步输出:
这是因为 ngForm
只会将标有 ngModel
的子元素的值添加到数据模型中。我们为一个”用户名”这个子元素添加 ngModel
:
1 | <form #myForm="ngForm" action="/createUser" method="post" ngForm> |
结果显而易见:
刚才我们注意到,点击“注册”按钮页面是没有任何反应的。这是因为提交会导致页面刷新,而 SPA 应用不能让页面刷新,因此 Angular 会拦截 HTML 的表单提交事件,而使用 ngSubmit 事件来代替:
template-form.component.html
1 | <form #myForm="ngForm" (ngSubmit)="createUser(myForm.value)"> |
template-form.component.ts
1 | export class TemplateFormComponent implements OnInit { |
然后我们在页面中进行输入,然后点击“注册”,会发现后台打出相应字段的内容:
NgModel 指令
NgModel
指令代表表单的一个字段,它会隐式地创建一个 FormControl
实例代表字段的数据模型,FormControl
对象存储字段的值。
使用 NgModel
指令时,不需要使用 ()、[] 等将 NgModel
括起来,它不是一个双向绑定,而是表单字段的指令。也不需要绑定到组件的属性上,但需要为添加 NgModel
的 HTML
元素为它指定一个 name
属性和属性值。这个属性会成为 NgForm
对象 value
属性对应的对象中的属性。
与 NgForm
类似,NgModel
也可以通过模版变量来进行引用。
1 | <form #myForm="ngForm" action="/createUser" method="post" (ngSubmit)="createUser(myForm.value)"> |
NgModelGroup 指令
NgModelGroup
代表表单的一部分,它允许将表单的一部分组织在一起,使关系层次更清晰。
与 NgForm
相同,NgModelGroup
也会创建一个 NgGroup
类型的对象。它会在 NgForm
的 value 属性中,表现为一个嵌套对象。在这个对象中,我们也可以对其它字段加到嵌套的对象中。
最终写法:
1 | <form #myForm="ngForm" (ngSubmit)="createUser(myForm.value)"> |
响应式表单
响应式表单是通过编写 TypeScript 代码而不是 HTML 的代码来创建一个底层的数据模型,在这个模型定义好后使用一些特定的指令,将模版上的 HTML 元素与底层的数据模型连接在一起。
响应式表单指令
类名 | 属性绑定指令 | 属性名指令 |
---|---|---|
FormGroup | formGroup | formGroupName |
FormControl | formControl | formControlName |
FormArray | - | formArrayName |
这些指令全部来自于 ReactiveFormsModule
模块。第二列所示的“属性绑定指令”是指用方括号绑定的属性指令,属性名指令不需要使用属性绑定语法,它指属性的值。
所有响应式表单指令都是以 Form
开头的,而模版式表单是以 ng
开头的,因此可以通过查看模版判断是哪种表单。
注:
响应式表单指令是不可在通过在模版创建变量被引用的。模版式表单可以在模版中取到变量并进行操作,但不能在 ts 文件中拿到引用变量。与此相反,响应式表单可以在 ts 拿到引用的类的,却不能在模版中获取到变量,因此无法在模版中进行操作。
属性名指令要求该变量一定要被绑定到某个变量上,
formGroup
reactived-form.component.html
1 | <form [formGroup]="formModel" (click)="createUser()"> |
reactive-form.component.ts
1 | ... |
[formGroup]="formModel"
表示将 formGroup
绑定到 component 中一个名为 formModel
的属性上。
在模版表单中,我们用(ngSubmit)="createUser(myForm.value)"
向后台传入数据。但在响应式表单中,createUser() 中不需要传入数据,因为 ts 无法获取引用对象。其次,我们在 ts 中可以通过 formModel: formGroup;
拿到数据。
formGroupName、formControlName和formArrayName
1 | <form [formGroup]="formModel" (submit)="createUser()"> |
1 | import { Component, OnInit } from '@angular/core'; |
app.module.ts
1 | ... |
邮箱使用了 formArrayName
绑定,formModel.get('emails')
得到的是一个数组。由于其中的 FormControlName
在 ts 的模型中没有相应 key,因此需要使用方括号声明。
this.formModel.get('emails')
拿到的是 FormArray
、FormControl
和 FormGroup
共有的抽象类类型,因此要强转成 FormArray
类型。
formControl
在上面的例子中,我们并没有使用 formControl
属性。我们不能将 formControlNmae
改为 formControl
,是因为它和 formGroup
的用法一样,如果我们写做 [formControl]="nick"
表示将 formControl
绑定到 component 中一个名为 nick
的属性上,然而在我们的模型中并没有这样的属性。
我们可以在表单以外使用该属性,例如:
reactive-form.component.html
1 | <input [formControl]="xxx"> |
reactive-form.component.ts
1 | export class ReactivedFormComponent implements OnInit { |
FormBuilder
FormBuilder
与 FormArray
、FormControl
和 FormGroup
的功能完全相同,但它简化了语法。
我们来改写上面的 ts 文件:
new FormControl()
改写为 ['']
new FormArray()
改写为 this.fb.array()
new FormGroup()
改写为 this.fb.group()
FormBuilder
除了简化语法,还提供了一些校验方法。['']
第一个参数是 form 初始默认值,第二个值是校验方法,第三个是异步校验方法。
reactived-form-component.ts
1 | export class ReactivedFormComponent implements OnInit { |