Angular 提供了两种不同的方法来通过表单处理用户输入:
响应式
表单(可扩展性、可复用性和可测试性更强,更关注组件ts
)和模板驱动
表单(简单易用,更关注模板html
。个人觉得vue中的表单
应该更趋向这种)
模板驱动表单
hero-form.component.html
hero-form.component.ts
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-form',
templateUrl: './hero-form.component.html',
styleUrls: ['./hero-form.component.css']
})
export class HeroFormComponent {
powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'];
model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');
submitted = false;
onSubmit() { this.submitted = true; }
newHero() {
this.model = new Hero(42, '', '');
}
}
hero.ts
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}
响应式表单
下面文字来自:细说 Angular 2+ 的表单(二):响应式表单
作者:接灰的电子产品
链接:https://juejin.im/post/593fa043a0bb9f006b684df4
来源:掘金
用途:备份下,方便自己学习
响应式表单乍一看还是很像模板驱动型表单的,但响应式表单需要引入一个不同的模块: ReactiveFormsModule
而不是 FormsModule
import {ReactiveFormsModule} from "@angular/forms";
@NgModule({
......
imports: [..., ReactiveFormsModule],
......
})
......
与模板驱动型表单的区别
接下来我们还是利用前面的例子,用响应式表单的要求改写一下:
<form [formGroup]="user" (ngSubmit)="onSubmit(user)">
<label><span>电子邮件地址</span>
<input type="text" formControlName="email"
placeholder="请输入您的 email 地址">
</label>
<div class="error"
*ngIf="user.get('email').hasError('required') && user.get('email').touched">
email 是必填项
</div>
<div class="error"
*ngIf="user.get('email').hasError('pattern') && user.get('email').touched">
email 格式不正确
</div>
<div>
<label><span>密码</span>
<input type="password" formControlName="password"
placeholder="请输入您的密码">
</label>
<div class="error"
*ngIf="user.get('password').hasError('required') && user.get('password').touched" >
密码是必填项
</div>
<label><span>确认密码</span>
<input type="password" formControlName="repeat"
placeholder="请再次输入密码">
</label>
<div class="error"
*ngIf="user.get('repeat').hasError('required') && user.get('repeat').touched" >
确认密码是必填项
</div>
<div class="error"
*ngIf="user.hasError('validateEqual') && user.get('repeat').touched" >
确认密码和密码不一致
</div>
</div>
<div formGroupName="address">
<label> <span>省份</span>
<select formControlName="province">
<option value="">请选择省份</option>
<option [value]="province" *ngFor="let province of provinces">
{{province}}
</option>
</select>
</label>
<label><span>城市</span>
<select formControlName="city">
<option value="">请选择城市</option>
<option [value]="city" *ngFor="let city of (cities$ | async)">
{{city}}
</option>
</select>
</label>
<label><span>区县</span>
<select formControlName="area">
<option value="">请选择区县</option>
<option [value]="area" *ngFor="let area of (areas$ | async)">
{{area}}
</option>
</select>
</label>
<label><span>地址</span>
<input type="text" formControlName="addr">
</label>
</div>
<button type="submit" [disabled]="user.invalid">注册</button>
</form>
这段代码和模板驱动型表单的那段看起来差不多,但是有几个区别:
- 表单多了一个指令
[formGroup]="user"
- 去掉了对表单的引用
#f="ngForm"
- 每个控件多了一个
formControlName
- 但同时每个控件也去掉了验证条件,比如
required
、minlength
等 - 在地址分组中用
formGroupName="address"
替代了ngModelGroup="address"
模板上的区别大概就这样了,接下来我们来看看组件的区别:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from "@angular/forms";
@Component({
selector: 'app-model-driven',
templateUrl: './model-driven.component.html',
styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
ngOnInit() {
// 初始化表单
this.user = new FormGroup({
email: new FormControl('', [Validators.required,
Validators.pattern(/([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]
+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}/)]),
password: new FormControl('', [Validators.required]),
repeat: new FormControl('', [Validators.required]),
address: new FormGroup({
province: new FormControl(''),
city: new FormControl(''),
area: new FormControl(''),
addr: new FormControl('')
})
});
}
onSubmit({value, valid}){
if(!valid) return;
console.log(JSON.stringify(value));
}
}
从上面的代码中我们可以看到,这里的表单FormGroup
是由一系列的表单控件FormControl
构成的。其实 FormGroup
的构造函数接受的是三个参数: controls
(表单控件『数组』,其实不是数组,是一个类似字典的对象) 、 validator
(验证器) 和 asyncValidator
(异步验证器) ,其中只有 controls
数组是必须的参数,后两个都是可选参数。
// FormGroup 的构造函数
constructor(
controls: {
[key: string]: AbstractControl;
},
validator?: ValidatorFn,
asyncValidator?: AsyncValidatorFn
)
- 我们上面的代码中就没有使用验证器和异步验证器的可选参数,而且注意到我们提供
controls
的方式是,一个key
对应一个FormControl
- 比如下面的
key
是password
,对应的值是new FormControl('', [Validators.required])
- 这个
key
对应的就是模板中的formControlName
的值,我们模板代码中设置了formControlName="password"
- 而表单控件会根据这个
password
的控件名来跟踪实际的渲染出的表单页面上的控件(比如<input formcontrolname="password">
)的值和验证状态。
password: new FormControl('', [Validators.required])
- 那么可以看出,这个表单控件的构造函数同样也接受三个可选参数,分别是:
- 控件初始值(
formState
)、控件验证器或验证器数组(validator
)和控件异步验证器或异步验证器数组(asyncValidator
)。 - 上面的那行代码中,初始值为空字符串,验证器是『必选』,而异步验证器我们没有提供。
// FormControl 的构造函数
constructor(
formState?: any, // 控件初始值
validator?: ValidatorFn | ValidatorFn[], // 控件验证器或验证器数组
// 控件异步验证器或异步验证器数组
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]
)
由此可以看出,响应式表单区别于模板驱动型表单的的主要特点在于:是由组件类去创建、维护和跟踪表单的变化,而不是依赖模板
FormBuilder 快速构建表单
上面的表单构造起来虽然也不算太麻烦,但是在表单项目逐渐多起来之后还是一个挺麻烦的工作,所以Angular
提供了一种快捷构造表单的方式 -- 使用 FormBuilder
。
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
selector: 'app-model-driven',
templateUrl: './model-driven.component.html',
styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit() {
// 初始化表单
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
address: this.fb.group({
province: [],
city: [],
area: [],
addr: []
})
});
}
// 省略其他部分
}
- 使用
FormBuilder
我们可以无需显式声明FormControl
或FormGroup
。 -
FormBuilder
提供三种类型的快速构造:control
,group
和array
,分别对应FormControl
,FormGroup
和FormArray
。 - 我们在表单中最常见的一种是通过
group
来初始化整个表单。 - 上面的例子中,我们可以看到
group
接受一个字典对象作为参数,这个字典中的key
就是这个FormGroup
中FormControl
的名字,值是一个数组,数组中的第一个值是控件的初始值,第二个是同步验证器的数组,第三个是异步验证器数组(第三个并未出现在我们的例子中)。 - 这其实已经在隐性的使用
FormBuilder.control
了,可以参看下面的FormBuilder
中的control
函数定义,其实FormBuilder
利用我们给出的值构造了相对应的control
:
control(
formState: Object,
validator?: ValidatorFn | ValidatorFn[],
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]
): FormControl;
- 此外还值得注意的一点是
address
的处理,我们可以清晰的看到FormBuilder
支持嵌套,遇到FormGroup
时仅仅需要再次使用this.fb.group({...})
即可。 - 这样我们的表单在拥有大量的表单项时,构造起来就方便多了。
自定义验证
对于响应式表单来说,构造一个自定义验证器是非常简单的,比如我们上面提到过的的验证 密码
和 重复输入密码
是否相同的需求,我们在响应式表单中来试一下。
validateEqual(passwordKey: string, confirmPasswordKey: string): ValidatorFn {
return (group: FormGroup): {[key: string]: any} => {
const password = group.controls[passwordKey];
const confirmPassword = group.controls[confirmPasswordKey];
if (password.value !== confirmPassword.value) {
return { validateEqual: true };
}
return null;
}
}
- 这个函数的逻辑比较简单:我们接受两个字符串(是 FormControl 的名字),然后返回一个
ValidatorFn
。 - 但是这个函数里面就奇奇怪怪的,比如
(group: FormGroup): {[key: string]: any} => {...}
是什么意思啊?还有,这个ValidatorFn
是什么鬼?我们来看一下定义:
export interface ValidatorFn {
(c: AbstractControl): ValidationErrors | null;
}
- 这样就清楚了,
ValidatorFn
是一个对象定义,这个对象中有一个方法 - 此方法接受一个
AbstractControl
类型的参数(其实也就是我们的 FormControl,而 AbstractControl 为其父类), - 而这个方法还要返回
ValidationErrors
,这个ValidationErrors
的定义如下:
export declare type ValidationErrors = {
[key: string]: any;
};
- 回过头来再看我们的这句
(group: FormGroup): {[key: string]: any} => {...}
,大家就应该明白为什么这么写了,我们其实就是在返回一个ValidatorFn
类型的对象。 - 只不过我们利用
javascript/typescript
对象展开的特性把ValidationErrors
写成了{[key: string]: any}
。
弄清楚这个函数的逻辑后,我们怎么使用呢?非常简单,先看代码:
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
address: this.fb.group({
province: [],
city: [],
area: [],
addr: []
})
}, {validator: this.validateEqual('password', 'repeat')});
- 和最初的代码相比,多了一个参数,那就是
{validator: this.validateEqual('password', 'repeat')}
。 -
FormBuilder
的group
函数接受两个参数, - 第一个就是那串长长的,我们叫它
controlsConfig
,用于表单控件的构造,以及每个表单控件的验证器。但是如果一个验证器是要计算多个field
的话,我们可以把它作为整个group
的验证器。 - 所以 FormBuilder 的
group
函数还接收第二个参数,这个参数中可以提供同步验证器或异步验证器。同样还是一个字典对象,是同步验证器的话,key
写成 validator,异步的话写成asyncValidator
。
现在我们可以保存代码,启动 ng serve
到浏览器中看一下结果了:
FormArray 有什么用?
我们在购物网站经常遇到需要维护多个地址,因为我们有些商品希望送到公司,有些需要送到家里,还有些给父母采购的需要送到父母那里。
这就是一个典型的 FormArray 可以派上用场的场景。
所有的这些地址的结构都是一样的,有省、市、区县和街道地址,那么对于处理这样的场景,我们来看看在响应式表单中怎么做---。
首先,我们需要把 HTML 模板改造一下,现在的地址是多项了,所以我们需要在原来的地址部分外面再套一层,并且声明成
formArrayName="addrs"
。FormArray
顾名思义是一个数组,所以我们要对这个控件数组做一个循环,然后让每个数组元素是FormGroup
,只不过这次我们的[formGroupName]="i"
是让formGroupName
等于该数组元素的索引。
<div formArrayName="addrs">
<button (click)="addAddr()">Add</button>
<div *ngFor="let item of user.controls['addrs'].controls; let i = index;">
<div [formGroupName]="i">
<label>
<span>省份</span>
<select formControlName="province">
<option value="">请选择省份</option>
<option [value]="province" *ngFor="let province of provinces">{{province}}</option>
</select>
</label>
<label>
<span>城市</span>
<select formControlName="city">
<option value="">请选择城市</option>
<option [value]="city" *ngFor="let city of (cities$ | async)">{{city}}</option>
</select>
</label>
<label>
<span>区县</span>
<select formControlName="area">
<option value="">请选择区县</option>
<option [value]="area" *ngFor="let area of (areas$ | async)">{{area}}</option>
</select>
</label>
<label>
<span>地址</span>
<input type="text" formControlName="street">
</label>
</div>
</div>
</div>
改造好模板后,我们需要在类文件中也做对应处理,去掉原来的 address: this.fb.group({...})
,换成 addrs: this.fb.array([])
。
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
addrs: this.fb.array([])
}, {validator: this.validateEqual('password', 'repeat')});
但这样我们是看不到也增加不了新的地址的,因为我们还没有处理添加的逻辑呢,下面我们就添加一下:其实就是建立一个新的 FormGroup
,然后加入 FormArray
数组中。
addAddr(): void {
(<FormArray>this.user.controls['addrs']).push(this.createAddrItem());
}
private createAddrItem(): FormGroup {
return this.fb.group({
province: [],
city: [],
area: [],
street: []
})
}
到这里我们的结构就建好了,保存后,到浏览器中去试试添加多个地址吧!
响应式表单的优势
首先是可测试能力。模板驱动型表单进行单元测试是比较困难的,因为验证逻辑是写在模板中的。但验证器的逻辑单元测试对于响应式表单来说就非常简单了,因为你的验证器无非就是一个函数而已。
当然除了这个优点,我们对表单可以有完全的掌控:从初始化表单控件的值、更新和获取表单值的变化到表单的验证和提交,这一系列的流程都在程序逻辑控制之下。
而且更重要的是,我们可以使用函数响应式编程的风格来处理各种表单操作,因为响应式表单提供了一系列支持 Observable
的接口 API 。那么这又能说明什么呢?有什么用呢?
首先是无论表单本身还是控件都可以看成是一系列的基于时间维度的数据流了,这个数据流可以被多个观察者订阅和处理,由于 valueChanges
本身是个 Observable
,所以我们就可以利用 RxJS 提供的丰富的操作符,将一个对数据验证、处理等的完整逻辑清晰的表达出来。当然现在我们不会对 RxJS 做深入的讨论,后面有专门针对 RxJS 进行讲解的章节。
this.form.valueChanges
.filter((value) => this.user.valid)
.subscribe((value) => {
console.log("现在时刻表单的值为 ",JSON.stringify(value));
});
上面的例子中,我们取得表单值的变化,然后过滤掉表单存在非法值的情况,然后输出表单的值。这只是非常简单的一个 Rx 应用,随着逻辑复杂度的增加,我们后面会见证 Rx 卓越的处理能力。