在 Angular
当中存在两种表单处理的方式,模版式表单和响应式表单,它们两者对于表单的的处理方式是有所不同的,下面我们就慢慢来进行了解
它们两者的区别是
- 不管是哪种表单,都有一个对应的数据模型来存储表单的数据,在模版式表单中,数据模型是由
Angular
基于你组件模版中的指令隐式创建的,而在响应式表单中,你通过编码明确的创建数据模型然后将模版上的HTML
元素与底层的数据模型连接在一起 - 数据模型并不是一个任意的对象,它是一个由
angular/forms
模块中的一些特定的类,如FormControl
,FormGroup
,FormArray
等组成的,在模版式表单中,你是不能直接访问到这些类的 - 响应式表单并不会替你生成
HTML
,模版仍然需要你自己来编写,响应式表单不能在模版当中去操作数据模型,只能在代码中操作,模版式表单不能在代码中去操作,只能在模版当中操作
模版式表单(模版驱动表单)
表单的数据模型是通过组件模版中的相关指令来定义的,因为使用这种方式定义表单的数据模型的时候,我们会受限于 HTML
的语法,所以模版驱动方式只适合用于一些简单的场景,它主要包括这样几个指令 NgForm
,NgModel
,NgModelGroup
,下面我们就一个一个来看
NgForm
使用 NgForm
用来代表整个表单,在 Angular
应用中会被『自动的』添加到 form
元素上,不过需要注意的是,不仅限于 form
元素,对于 div
元素如果手动指定 ngForm
效果也是一样的,NgForm
指令隐式的创建了一个 FormGroup
类的实例,这个类用来代表表单的数据模型并且存储表单的数据
1 | <form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)"> |
NgModel
NgModel
代表表单中的一个字段,这个指令会隐式的创建一个 FormControl
的实例来代表字段模型,并用这个 FormControl
类型的对象来存储字段的值,比如上面的示例,在 input
当中输入的值并不会反应在下方,这是因为 input
标签并没有绑定 ngModel
指令,不过需要注意的是,绑定的时候直接使用 ngModel
即可,不需要添加任何括号,但是同时需要为绑定的元素添加一个 name
属性
1 | <div>用户名:<input type="text" ngModel name="username"></div> |
也可以单独的绑定 ngModel
1 | <div>用户名:<input #username="ngModel" type="text" ngModel name="username"></div> |
NgModelGroup
NgModelGroup
代表的是表单的一部分,它允许你将一些表单字段组织在一起形成更清晰的层次关系,和上面一样,也会创建一个 FormGroup
类的一个实例,这个实例会在 NgForm
对象的 value
属性中表现为一个嵌套的对象
1 | <div ngModelGroup="userinfo"> |
生成的数据为
1 | { |
响应式表单
其实在实际开发过程当中,模版式表单的使用是比较少的,大多都是响应式表单,因为使用响应式表单可以让我们更为随心所欲的控制每一个输入的值,所以在这里我们将会重点介绍响应式表单
在使用响应式表单时,是通过编写 TypeScript
代码而不是 HTML
代码来创建一个底层的数据模型,在这个模型定义好了以后,可以使用一些特定的指令,将模版上的 HTML
元素与底层的数据模型连接在一起,若使用模版式表单表单,则导入 FormsModule
,若使用响应式表单,则导入 ReactiveFormsModule
,与模版式表单不同,创建一个响应式表单需要两步
- 首先需要创建一个数据模型,用来保存表单数据的数据结构,简称模型,它由定义在
Angular
中的forms
模块中的三个类组成FormControl
,FormGroup
和FormArray
- 然后需要使用一些指令将模版中的
HTML
元素连接到这个数据模型上
响应式表单的指令
响应式表单使用一组与模版式表单完全不同的指令(全部来源于 ReactiveFormModule
模块)
类名 | 指令(这一列的需要使用属性绑定语法) | 指令(这一列不需要使用属性绑定语法) |
---|---|---|
FormGroup |
formGroup |
formGroupName |
FormControl |
formControl |
formControlName |
FormArray |
formArrayName |
响应式表单中所有的指令都是以 form
开头的,所以可以很容易的和模版式表单(比如 ngModel
)区分开来,这些 form
开头的指令是不能进行引用的(比如 #myForm="ngForm"
),模版式表单当中拿不到 FormControl
,FormGroup
和 FormArray
这三个类,而在响应式表单当中可以直接访问数据模型相关的类(由于它们是不可以引用的,所以不能在模版当中去操作数据模型,只能在代码当中操作)
FormGroup
既可以代表表单的一部分,也可以用于代表整个表单,它是多个 FormControl
的集合,FormGroup
将多个 FormControl
的值和状态聚合在一起,比如在表单验证中,如果其中一个 FormControl
是无效的,那么整个 FormGroup
就是无效的
FormArray
与 FormGroup
类似,但是有一个额外的长度属性,一般来说,FormGroup
用来代表整个表单或者表单字段的一个固定子集,而 FormArray
通常用来代表一个可以增长的字段集合,但是它里面的字段是没有 key
属性的,只能通过序列去查询
FormControl
它是构成表单的基本单位,通常情况下会用来代表一个 input
元素,但是也可以用来代表一个更为复杂的 UI
组件,比如日历,下拉选择块等,它保存着与其关联的 HTML
元素当前的值以及元素的校验状态,还有元素是否被修改过的相关信息
1 | export class ReactiveFormComponent implements OnInit { |
指令的具体作用
formGroup
一般我们会使用绑定到一个 form
标签的 formGroup
对象来代表整个表单,比如
1 | <form [formGroup]="formModel"></form> |
这样一来,表单的处理方式就会变成一个响应式表单的处理方式
formGroupName
在模版当中使用 formGroupName
来连接一个 formGroup
,比如 formGroupName='dataRange'
,在组件中使用 FormGroup
来构造对应的指定名称
formControlName
必须声明在一个 formGroup
之内来连接 formGroup
之内的 formControl
和页面上的 DOM
元素
formArrayName
同 formControlName
类似,同样必须用在 formGroup
之内,因为在 formArrayName
当中没有序列号,所以一般和 *ngFor
指令配合使用
formControl
不能使用在模版当中的 formGroup
的内部,只能用在外部与某个单独的元素(input
)绑定起来
再次强调
- 在响应式表单当中,所有的指令都是以
form
开头的(模版式表单才是以ngxxx
开头) - 如果指令以
Name
结尾,不需要使用属性绑定的语法,直接等于一个属性的名称即可(字符串),同时,这些属性只能用在formGroup
覆盖的范围之内 - 如果指令不是以
Name
结尾,则需要使用属性绑定的语法([]=""
)
使用 FormBuild 简化写法
使用 FormBuild
简化了定义表单结构的语法,相对于直接使用 FormGroup
,FormControl
和 FormArray
,它可以让我们使用更少的代码定义出同样的数据结构,来重构上面的示例
1 | formModel: FormGroup |
使用 FormBuilder
可以简化我们的代码,同时提供了更多了配置,比如 fb.group({})
方法,调用其就相当于 new FormGroup({})
,但是其还可以接收一个额外的参数用来校验这个 formGroup
,而对于其中的 formControl
则采用了一个数组(['']
)的形式来进行初始化,同时还可以额外接收两个参数
1 | // 如果多于三个参数,其他的元素会被忽略 |
完整示例
前提需要在当前模块下导入 ReactiveFormsModule
并且在 imports
当中进行添加
1 | import { ReactiveFormsModule } from '@angular/forms' |
模版如下
1 | <form [formGroup]="formModel" (submit)="onSubmit()"> |
组件如下
1 | import { FormGroup, FormControl, FormArray } from '@angular/forms' |
自定义表单控件
我们来尝试着将一个普通的模版封装为自定义表单控件,需要首先引入 ControlValueAccessor
,然后将接口定义为 ControlValueAccessor
,其内部有三个方法,需要我们自己去手动实现
1 | import { ControlValueAccessor } from '@angular/forms' |
然后需要指定依赖池
1 | import { forwardRef } from '@angular/core' |
添加自定义认证
1 | validate(c: FormControl): {[key: string]: any} { |
完整代码如下
1 | <!-- 模版 --> |
1 | import { Component, Input, forwardRef } from '@angular/core' |
使用
1 | <!-- 选择头像 --> |
这样一来就可以进行初始化操作了
1 | import { FormBuilder, FormGroup } from '@angular/forms' |