Angular 中的表单

Angular 中的表单

Angular 当中存在两种表单处理的方式,模版式表单和响应式表单,它们两者对于表单的的处理方式是有所不同的,下面我们就慢慢来进行了解

它们两者的区别是

  • 不管是哪种表单,都有一个对应的数据模型来存储表单的数据,在模版式表单中,数据模型是由 Angular 基于你组件模版中的指令隐式创建的,而在响应式表单中,你通过编码明确的创建数据模型然后将模版上的 HTML 元素与底层的数据模型连接在一起
  • 数据模型并不是一个任意的对象,它是一个由 angular/forms 模块中的一些特定的类,如 FormControlFormGroupFormArray 等组成的,在模版式表单中,你是不能直接访问到这些类的
  • 响应式表单并不会替你生成 HTML,模版仍然需要你自己来编写,响应式表单不能在模版当中去操作数据模型,只能在代码中操作,模版式表单不能在代码中去操作,只能在模版当中操作

模版式表单(模版驱动表单)

表单的数据模型是通过组件模版中的相关指令来定义的,因为使用这种方式定义表单的数据模型的时候,我们会受限于 HTML 的语法,所以模版驱动方式只适合用于一些简单的场景,它主要包括这样几个指令 NgFormNgModelNgModelGroup,下面我们就一个一个来看

NgForm

使用 NgForm 用来代表整个表单,在 Angular 应用中会被『自动的』添加到 form 元素上,不过需要注意的是,不仅限于 form 元素,对于 div 元素如果手动指定 ngForm 效果也是一样的,NgForm 指令隐式的创建了一个 FormGroup 类的实例,这个类用来代表表单的数据模型并且存储表单的数据

1
2
3
4
5
6
7
8
9
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
<div>用户名:<input type="text"></div>
<div>密码:<input type="text"></div>
<button type="submit">登录</button>
</form>

<div>
{{myForm.value | json}}
</div>

NgModel

NgModel 代表表单中的一个字段,这个指令会隐式的创建一个 FormControl 的实例来代表字段模型,并用这个 FormControl 类型的对象来存储字段的值,比如上面的示例,在 input 当中输入的值并不会反应在下方,这是因为 input 标签并没有绑定 ngModel 指令,不过需要注意的是,绑定的时候直接使用 ngModel 即可,不需要添加任何括号,但是同时需要为绑定的元素添加一个 name 属性

1
<div>用户名:<input type="text" ngModel name="username"></div>

也可以单独的绑定 ngModel

1
2
3
<div>用户名:<input #username="ngModel" type="text" ngModel name="username"></div>

<div>{{username.value}}</div>

NgModelGroup

NgModelGroup 代表的是表单的一部分,它允许你将一些表单字段组织在一起形成更清晰的层次关系,和上面一样,也会创建一个 FormGroup 类的一个实例,这个实例会在 NgForm 对象的 value 属性中表现为一个嵌套的对象

1
2
3
4
5
<div ngModelGroup="userinfo">
<div>用户名:<input #username="ngModel" type="text" ngModel name="username"></div>
</div>

<div>{{username.value}}</div>

生成的数据为

1
2
3
4
5
{
'userinfo': {
'username': ''
}
}

响应式表单

其实在实际开发过程当中,模版式表单的使用是比较少的,大多都是响应式表单,因为使用响应式表单可以让我们更为随心所欲的控制每一个输入的值,所以在这里我们将会重点介绍响应式表单

在使用响应式表单时,是通过编写 TypeScript 代码而不是 HTML 代码来创建一个底层的数据模型,在这个模型定义好了以后,可以使用一些特定的指令,将模版上的 HTML 元素与底层的数据模型连接在一起,若使用模版式表单表单,则导入 FormsModule,若使用响应式表单,则导入 ReactiveFormsModule,与模版式表单不同,创建一个响应式表单需要两步

  • 首先需要创建一个数据模型,用来保存表单数据的数据结构,简称模型,它由定义在 Angular 中的 forms 模块中的三个类组成 FormControlFormGroupFormArray
  • 然后需要使用一些指令将模版中的 HTML 元素连接到这个数据模型上

响应式表单的指令

响应式表单使用一组与模版式表单完全不同的指令(全部来源于 ReactiveFormModule 模块)

类名 指令(这一列的需要使用属性绑定语法) 指令(这一列不需要使用属性绑定语法)
FormGroup formGroup formGroupName
FormControl formControl formControlName
FormArray formArrayName

响应式表单中所有的指令都是以 form 开头的,所以可以很容易的和模版式表单(比如 ngModel)区分开来,这些 form 开头的指令是不能进行引用的(比如 #myForm="ngForm"),模版式表单当中拿不到 FormControlFormGroupFormArray 这三个类,而在响应式表单当中可以直接访问数据模型相关的类(由于它们是不可以引用的,所以不能在模版当中去操作数据模型,只能在代码当中操作)

FormGroup

既可以代表表单的一部分,也可以用于代表整个表单,它是多个 FormControl 的集合,FormGroup 将多个 FormControl 的值和状态聚合在一起,比如在表单验证中,如果其中一个 FormControl 是无效的,那么整个 FormGroup 就是无效的

FormArray

FormGroup 类似,但是有一个额外的长度属性,一般来说,FormGroup 用来代表整个表单或者表单字段的一个固定子集,而 FormArray 通常用来代表一个可以增长的字段集合,但是它里面的字段是没有 key 属性的,只能通过序列去查询

FormControl

它是构成表单的基本单位,通常情况下会用来代表一个 input 元素,但是也可以用来代表一个更为复杂的 UI 组件,比如日历,下拉选择块等,它保存着与其关联的 HTML 元素当前的值以及元素的校验状态,还有元素是否被修改过的相关信息

1
2
3
4
export class ReactiveFormComponent implements OnInit {
// FormControl 这个构造函数可以接收一个参数,用来指定 FormControl 的初始值
username: FormControl = new FormControl('zhangsan')
}

指令的具体作用

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 简化了定义表单结构的语法,相对于直接使用 FormGroupFormControlFormArray,它可以让我们使用更少的代码定义出同样的数据结构,来重构上面的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
formModel: FormGroup

constructor(private fb: FormBuilder) {
this.formModel = fb.group({
username: ['zhangsan'],
phone: [''],
dateRange: fb.group({
from: [''],
to: ['']
}),
emails: fb.array(['123@126.com', '456@126.com'])
})
}

使用 FormBuilder 可以简化我们的代码,同时提供了更多了配置,比如 fb.group({}) 方法,调用其就相当于 new FormGroup({}),但是其还可以接收一个额外的参数用来校验这个 formGroup,而对于其中的 formControl 则采用了一个数组([''])的形式来进行初始化,同时还可以额外接收两个参数

1
2
// 如果多于三个参数,其他的元素会被忽略
username: ['初始值', 校验方法, 异步的校验方法],

完整示例

前提需要在当前模块下导入 ReactiveFormsModule 并且在 imports 当中进行添加

1
2
3
4
5
6
7
8
import { ReactiveFormsModule } from '@angular/forms'

@NgModule({
// ...
imports: [
ReactiveFormsModule
]
})

模版如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<form [formGroup]="formModel" (submit)="onSubmit()">
<!--
使用 formControl 来进行绑定,但是这样 username 属性进入不到表单内部,所以可以将其放入到 formGroup 内部,同时改变绑定写法(否则会报错)
在外部 <input type="text" [formControl]="username">
在内部 <input type="text" formControlName="username">
然后在组件中将 username 移动至 formModel 当中即可
-->
<input formControlName="username">
<input formControlName="phone">
<!-- 使用 formGroupName 来指定组件当中的 formGroup 名称 -->
<div formGroupName="dateRange">
起始日期:<input type="date" formControlName="from">
结束日期:<input type="date" formControlName="to">
</div>
<div>
<ul formArrayName="emails">
<!-- 获得 formModel 当中 emails,然后使用 controls 来获取当中的集合(即数组) -->
<li *ngFor="let email of this.formModel.get('emails').controls; let i = index;">
<!-- 使用 formControlName 将其和循环下标绑定在一起,注意需要使用属性绑定语法 -->
<input type="text" [formControlName]="i">
</li>
</ul>
<button type="button" (click)="addEmail()">新增 Email</button>
</div>
<button type="submit">保存</button>
</form>

组件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { FormGroup, FormControl, FormArray } from '@angular/forms'

export class ReactiveFormComponent implements OnInit {
// FormControl 这个构造函数可以接收一个参数,用来指定 FormControl 的初始值
username: FormControl = new FormControl('zhangsan')

formModel: FormGroup = new FormGroup({
username: new FormControl('zhangsan'),
dateRange: new FormGroup({
from: new FormControl(),
to: new FormControl()
}),
emails: new FormArray([
new FormControl('123@126.com'),
new FormControl('456@126.com')
])
})

constructor() { }

ngOnInit() { }

onSubmit() {
console.log(this.formModel.value)
}

addEmail() {
// 拿到了是一个 FormArray 类型的对象,所以强制转换一下类型
const emails = this.formModel.get('emails') as FormArray
// 点击新增的时候添加一个 input
emails.push(new FormControl())
}
}

自定义表单控件

我们来尝试着将一个普通的模版封装为自定义表单控件,需要首先引入 ControlValueAccessor,然后将接口定义为 ControlValueAccessor,其内部有三个方法,需要我们自己去手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { ControlValueAccessor } from '@angular/forms'

export class ImageListSelectComponent implements ControlValueAccessor {

public propagateChange = (_: any) => {}

// 这个方法用来写入值,就像之前的设置初始值,或者在方法内部写入值,比如 this.form.patchValue 或者 this.form.setValue
writeValue(obj: any): void {
this.selected = obj
}

// 如果表单的 value 或者值发生了变化,需要通知表单,定义一个空函数接收系统传递的一个函数在表单发生变化的时候 emit 这个事件通知表单需要进行更新
registerOnChange(fn: any): void {

}

// 指明表单控件什么情况下算是 Touch 状态,需要告诉给表单定义一个空函数来进行接收
registerOnTouched(fn: any): void {
this.propagateChange = fn
}

// 然后在发生变化的时候,emit 通知表单发生了变化
onChange(i) {
this.propagateChange(this.selected)
}

}

然后需要指定依赖池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { forwardRef } from '@angular/core'
// 引入 NG_VALUE_ACCESSOR 令牌
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'

@Component({
providers: [
{
// 将自身注册到这个令牌上
provide: NG_VALUE_ACCESSOR,
// useExisting 使用已有的,也就是自身
// 然后这里存在一个问题,在元数据当中,自身可能没有被创建,所以注册不到令牌上面,所以这里使用 forwardRef() 方法,就可以引用自身了
// 会等待实例化之后才会进行引用,这样写不影响使用 useExisting 注册到依赖池当中
useExisting: forwardRef(() => ImageListSelectComponent),
// 类似 NG_VALUE_ACCESSOR 这种令牌,本身都是多对一的,比如多个控件使用同一个令牌
multi: true
},
// 验证同理,也需要是可以验证的
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ImageListSelectComponent),
multi: true
}
]
})

添加自定义认证

1
2
3
4
5
6
7
validate(c: FormControl): {[key: string]: any} {
return this.selected ? null : {
imageListInvalid: {
valid: false
}
}
}

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 模版 -->
<div>
<span>{{title}}</span>
<img [src]="selected" class="avatar">
</div>
<div class="scroll-container">
<md-grid-list [cols]="cols" [rowHeight]="rowHeight">
<md-grid-tile *ngFor="let item of items; let i = index">
<div class="image-container" (click)="onChange(i)">
<img class="avatar" [src]="item">
<div class="after">
<div class="zoom">
<md-icon>checked</md-icon>
</div>
</div>
</div>
</md-grid-tile>
</md-grid-list>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { Component, Input, forwardRef } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl } from '@angular/forms'

@Component({
selector: 'app-image-list-select',
templateUrl: './image-list-select.component.html',
styleUrls: ['./image-list-select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ImageListSelectComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ImageListSelectComponent),
multi: true
}
]
})

export class ImageListSelectComponent implements ControlValueAccessor {

@Input() title = '选择'
@Input() cols = '6'
@Input() rowHeight = '64px'
@Input() items: string[] = []

public selected: string
public propagateChange = (_: any) => { }

constructor() { }

onChange(i) {
this.selected = this.items[i]
this.propagateChange(this.selected)
}

writeValue(obj: any): void {
this.selected = obj
}

registerOnChange(fn: any): void {
this.propagateChange = fn
}

registerOnTouched(fn: any): void { }

validate(c: FormControl): { [key: string]: any } {
return this.selected ? null : {
imageListInvalid: {
valid: false
}
}
}
}

使用

1
2
3
4
5
6
<!-- 选择头像 -->
<app-image-list-select
[cols]="6"
[items]="items"
formControlName="avatar"
></app-image-list-select>

这样一来就可以进行初始化操作了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { FormBuilder, FormGroup } from '@angular/forms'

export class RegisterComponent implements OnInit {

public items: string[] = [
'assets/avatar/01.jpg',
'assets/avatar/02.jpg',
'assets/avatar/03.jpg',
'assets/avatar/04.jpg'
]
form: FormGroup

constructor(private fb: FormBuilder) { }

ngOnInit() {
this.form = this.fb.group({
email: [],
name: [],
password: [],
repeat: [],
avatar: ['assets/1.jpg']
})
}
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×