Angular表单
背景
Angular相关内容一个文件写不下,分开在多个文档里可能更好一些.
简介
表单作为web里的一个基础元素,要点并不多
- 表单在html页面上的写法
- 表单值在后台的获取
- 表单的验证
- 验证结果的表达
而Angular作为一个MVVM框架,将表单中的每个字段都与一个 FormControl
对象绑定在一起.
这些对象里包含了字段的值,字段的验证状态等等元素.
通常表单里有多个字段,为了方便统一管理,又有了 FormGroup
与 FormArray
两个对象.
这两个对象里可以额外存储一些跨字段验证的状态和信息.
其中 FormArray
还可以动态地加入字段.
上面这种方法称为 响应式表单
,
有时Angular想方便一些简单场景下的使用,于是以响应式表单为底层,
使用指令实现了又一种表单, 模板驱动表单
.
Angular中提到表单,可能会关注到
- FormControl对象的定义,方法,绑定到模板
- 使用formBuilder快捷创建
- 自定义验证函数
- 验证结果的表达
响应式表单
使用响应式表单,需要在模块文件中导入 ReactiveFormsModule
,
不然之后的许多地方会提示未定义.
原生创建
简单的场景:就一个字段
模板中
1 | <label> |
组件中
1 | export class XXXComponent { |
稍微复杂一些的场景: 使用了FormGroup统一管理
组件中
1 | profileForm = new FormGroup({ |
模板中
1 | <!-- 这里绑定组件中的FormGroup --> |
使用FormBuilder创建
表单会有更复杂的情况,比如嵌套,此时如果再用原生创建方法,就变得非常麻烦.
官方提供了FormBuilder来创建表单
1 | export class ProfileEditorComponent { |
模板中
1 | <!-- 绑定了表单提交时调用的函数 --> |
表单的验证
-
简单的验证
可以在formBuilder中定义要使用的验证条件
1
2
3
4
5
6
7this.form = this.fb.group({
username: ['', Validators.required], // 一个规则可以直接写
password: ['', [ // 多个规则可以放进数组传递
Validators.required,
Validators.minLength(6),
]]
})另外html5的原生验证,其结果也能被formControl收到.因此也可以使用.
个人也更偏好使用原生验证,此时的模板能够更完整地表达意图.1
<input type="text" formControlName="firstName" required minLength="4">
-
自定义验证
响应式表单的自定义验证靠函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16this.form = this.fb.group({
password: ['', this.forbiddenPassValidator(/123456/i)]
})
// 不知道为什么自定义验证需要返回的是一个函数
// 可能是不想让第三方的参数比如 nameRe 污染了后面 (AbstractControl => ValidationErrors|null) 这个函数的参数列表
// 故意要求柯里化
forbiddenPassValidator(nameRe: RegExp): ValidatorFn {
// 输入抽象的表单对象,可以是FormGroup, FormArray, FormControl
// 输出错误或null
// 错误的格式是 {errorName: {value: xxx}} 引用时使用 xxx.errorName.value
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? { forbiddenPass: { value: control.value } } : null;
};
} -
跨字段验证
在formBuilder定义时可以使用跨字段验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25this.form = this.fb.group({
username: [''],
password: ['']
}, { validators: this.cross1Validator });
// 如果不用到第三方参数,就可以直接定义一个类型是ValidatorFn的对象
cross1Validator: ValidatorFn = (fg: AbstractControl): ValidationErrors | null => {
const name = fg.get('username');
const pass = fg.get('password');
// 这里考虑了需要name和pass都不为空null情况下才算验证成功
return name && pass && name.value === pass.value ?
{ cross1: true } : null;
};
cross2Validator(): ValidatorFn {
return (fg: AbstractControl): ValidationErrors | null => {
// abstractControl太过抽象了,尽管不as也不报错,个人觉得还是as一下好
const fg1 = fg as FormGroup;
const name = fg1.get('username');
const pass = fg1.get('password');
return name && pass && name.value === pass.value ? { cross2: true } : null;
}
}; -
验证结果的使用
通常可以在html文件中使用ngIf等方式预制一些提示语句
1
2
3
4<div *ngIf="fc.username.invalid && (fc.username.dirty || fc.username.touched)">
<div *ngIf="fc.username.errors?.required">Name is required</div>
<div *ngIf="fc.username.errors?.minlength">minlength is 2</div>
</div>其中fc定义是
1
get fc() { return this.form.controls; }
然后就可以使用
fc.username
来获取一个字段了.
否则就需要使用form.get("username")
一个字段有许多状态
invalid:boolean
是否合法dirty:boolean
被修改touched:boolean
被选中errors:ValidationErrors
一个对象,比如{minLength: {value: 6}}
formGroup对象等等也有许多状态
- invalid
- dirty
- touched
- errors 这里会存放一些跨字段验证的结果
一个比较完整的例子
用于登陆的组件
1 | import { Component, OnInit } from '@angular/core'; |
对应的模板
1 | <h1>login</h1> |
模板驱动表单
使用模板驱动表单,需要在模块中引入 FormsModule
模块
简单定义
模板驱动表单的建立需要靠 ngModel
这一指令
1 | Favorite Color: <input type="text" [(ngModel)]="favoriteColor"> |
好处是ts文件中仅仅会看到一个简单的值
1 | export class FavoriteColorComponent { |
使用类的字段绑定
稍微复杂一些,表单的字段不仅仅可以绑定一个独立的值,
还可以绑定一个对象的某个属性,从而实现一个表单能够对应一个Model
1 | export class HeroFormTemplateComponent { |
模板中这样绑定
1 | <h1>模板驱动表单</h1> |
表单的验证
-
简单的验证
直接使用html5的原生验证方式
-
自定义验证
模板驱动表单的验证也是通过指令.
首先定义指令1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23({
selector: '[appForbiddenName]', // 声明了在模板中的使用方法
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}] // 需要注册一下
})
export class ForbiddenValidatorDirective implements Validator {
// 这里作为该指令接收参数的位置
// 参数之于函数,类似于input的属性之于这个类
'appForbiddenName') forbiddenName: string; (
// validator接口需要实现validate方法
validate(control: AbstractControl): ValidationErrors | null {
return this.forbiddenName ?
forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
: null;
}
}
// 这里的验证函数就能和响应式表单复用了
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {forbiddenName: {value: control.value}} : null;
};
}然后就可以在模板中使用了
1
2
3<input name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" > -
跨字段验证
同样的,跨字段验证也需要使用指令来实现
1
2
3
4
5
6
7
8
9
10({
selector: '[appIdentityRevealed]',
providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }]
})
export class IdentityRevealedValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors|null {
// 这里的函数也能复用
return identityRevealedValidator(control);
}
}然后在整个表单对象的定义域使用
1
<form #heroForm="ngForm" appIdentityRevealed>
-
验证结果
模板驱动表单不再能使用controls等方式来获取一个表单组里的formControl对象,
于是只能通过另一种使用#
的方式来引用页面上的一个变量,
从而获得invalid等状态来展示验证结果.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<form #heroForm="ngForm" appIdentityRevealed>
<label for="name">Name</label>
<!-- 下面的 #name="ngModel"将表单的控件绑定到了名为name的变量上 -->
<input name="name" required minlength="4" appForbiddenName="bob" [(ngModel)]="hero.name" #name="ngModel">
<!-- 所以这里才可以使用nave.invalid等变量 -->
<div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger">
<div *ngIf="name.errors.required">Name is required.</div>
<div *ngIf="name.errors.minlength">Name must be at least 4 characters long.</div>
<div *ngIf="name.errors.forbiddenName">Name cannot be Bob.</div>
</div>
<!-- 其他部分略 -->
<!-- 这里的heroForm也需要在上面用#声明好 -->
<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)">
Name cannot match alter ego.
</div>
</form>
表示状态的CSS
Angular预先定义了许多css类名,会自动应用于对应状态的表单上,
不过css的内容还需要人自己来定义,比如
1 | .ng-valid[required], .ng-valid.required { |
响应式表单和模板驱动表单的区别
原理上的区别
访问方式不同,
响应式表单: 页面元素 直接与 FormControl 对象访问
模板驱动表单: 页面元素 经由 ngModel
指令,间接访问 FormControl 的值
因此数据的流动是不同的
响应式表单:
- 页面变化带动模型
- input的值变化
- 组件中由于有FormControl,直接可以看到值的变化
- 模型变化带动页面
- 使用setValue等方法改变了FormControl的值
- input的值变化
模板驱动表单:
- 页面变化带动模型
- input值变化
- FormControl实例变化
- 在下一个周期,ngModel发现了这一变化
- 组件里的值更新了
- 模型变化带动页面
- 组件里的值更新了
- 在下一个周期,ngModel发现了这一变化
- FormControl实例变化
- input值变化
最直观的区别
模板驱动表单的自定义验证,直接写在模板中,可以看见.
而响应式表单的自定义验证是写在ts文件中,模板里看不见.