Angular 深入浅出之----响应式表单
首先说明:这是我看到的最好的讲解响应式表单的文章,到今天是第三遍,终于全部弄懂了。感谢原作者。 响应式表单乍一看还是很像模板驱动型表单的,但响应式表单需要引入一个不同的模块: 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 *ngIf="user.get('email').hasError('required') && user.get('email').touched" class="error">
email 是必填项
</div>
<div *ngIf="user.get('email').hasError('pattern') && user.get('email').touched" class="error">
email 格式不正确
</div>
<div>
<label>
<span>密码</span>
<input type="password" formControlName="password" placeholder="请输入您的密码">
</label>
<div *ngIf="user.get('password').hasError('required') && user.get('password').touched" class="error">
密码是必填项
</div>
<label>
<span>确认密码</span>
<input type="password" formControlName="repeat" placeholder="请再次输入密码">
</label>
<div *ngIf="user.get('repeat').hasError('required') && user.get('repeat').touched" class="error">
确认密码是必填项
</div>
<div *ngIf="user.hasError('validateEqual') && user.get('repeat').touched" class="error">
确认密码和密码不一致
</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>
这段代码和模板驱动型表单的那段看起来差不多,但是有几个区别:
模板上的区别大概就这样了,接下来我们来看看组件的区别: 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('',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 的构造函数
constructor(
controls: {
[key: string]: AbstractControl;
},validator?: ValidatorFn,asyncValidator?: AsyncValidatorFn
)
我们上面的代码中就没有使用验证器和异步验证器的可选参数,而且注意到我们提供 password: new FormControl('',[Validators.required])
那么可以看出,这个表单控件的构造函数同样也接受三个可选参数,分别是:控件初始值( // FormControl 的构造函数
constructor(
formState?: any,// 控件初始值
validator?: ValidatorFn | ValidatorFn[],// 控件验证器或验证器数组
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] // 控件异步验证器或异步验证器数组
)
由此可以看出,响应式表单区别于模板驱动型表单的的主要特点在于:是由组件类去创建、维护和跟踪表单的变化,而不是依赖模板。 那么我们是否在响应式表单中还可以使用 FormBuilder 快速构建表单上面的表单构造起来虽然也不算太麻烦,但是在表单项目逐渐多起来之后还是一个挺麻烦的工作,所以 Angular 提供了一种快捷构造表单的方式 -- 使用 FormBuilder。 import { Component,OnInit } from '@angular/core';
import { FormBuilder,styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit() {
// 初始化表单
this.user = this.fb.group({
email: ['',Validators.email]],password: ['',Validators.required],repeat: ['',address: this.fb.group({
province: [],city: [],area: [],addr: []
})
});
}
// 省略其他部分
}
使用 FormBuilder 我们可以无需显式声明 FormControl 或 FormGroup 。 FormBuilder 提供三种类型的快速构造: control(
formState: Object,validator?: ValidatorFn | ValidatorFn[],asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]
): FormControl;
此外还值得注意的一点是 address 的处理,我们可以清晰的看到 FormBuilder 支持嵌套,遇到 FormGroup 时仅仅需要再次使用 自定义验证对于响应式表单来说,构造一个自定义验证器是非常简单的,比如我们上面提到过的的验证 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 的名字),然后返回一个 export interface ValidatorFn {
(c: AbstractControl): ValidationErrors | null;
}
这样就清楚了, export declare type ValidationErrors = {
[key: string]: any;
};
回过头来再看我们的这句 弄清楚这个函数的逻辑后,我们怎么使用呢?非常简单,先看代码: this.user = this.fb.group({
email: ['',addr: []
})
},{validator: this.validateEqual('password','repeat')});
和最初的代码相比,多了一个参数,那就是 现在我们可以保存代码,启动 FormArray 有什么用?我们在购物网站经常遇到需要维护多个地址,因为我们有些商品希望送到公司,有些需要送到家里,还有些给父母采购的需要送到父母那里。这就是一个典型的 FormArray 可以派上用场的场景。所有的这些地址的结构都是一样的,有省、市、区县和街道地址,那么对于处理这样的场景,我们来看看在响应式表单中怎么做。 首先,我们需要把 HTML 模板改造一下,现在的地址是多项了,所以我们需要在原来的地址部分外面再套一层,并且声明成 <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>
改造好模板后,我们需要在类文件中也做对应处理,去掉原来的 this.user = this.fb.group({
email: ['',addrs: this.fb.array([])
},'repeat')});
但这样我们是看不到也增加不了新的地址的,因为我们还没有处理添加的逻辑呢,下面我们就添加一下:其实就是建立一个新的 FormGroup,然后加入 FormArray 数组中。 addAddr(): void {
(<FormArray>this.user.controls['addrs']).push(this.createAddrItem()); } private createAddrItem(): FormGroup { return this.fb.group({ province: [],city: [],area: [],street: [] }) }
到这里我们的结构就建好了,保存后,到浏览器中去试试添加多个地址吧! FormArray 处理结构相同的多组表单项响应式表单的优势首先是可测试能力。模板驱动型表单进行单元测试是比较困难的,因为验证逻辑是写在模板中的。但验证器的逻辑单元测试对于响应式表单来说就非常简单了,因为你的验证器无非就是一个函数而已。 当然除了这个优点,我们对表单可以有完全的掌控:从初始化表单控件的值、更新和获取表单值的变化到表单的验证和提交,这一系列的流程都在程序逻辑控制之下。 而且更重要的是,我们可以使用函数响应式编程的风格来处理各种表单操作,因为响应式表单提供了一系列支持 首先是无论表单本身还是控件都可以看成是一系列的基于时间维度的数据流了,这个数据流可以被多个观察者订阅和处理,由于 this.form.valueChanges
.filter((value) => this.user.valid)
.subscribe((value) => {
console.log("现在时刻表单的值为 ",JSON.stringify(value));
});
上面的例子中,我们取得表单值的变化,然后过滤掉表单存在非法值的情况,然后输出表单的值。这只是非常简单的一个 Rx 应用,随着逻辑复杂度的增加,我们后面会见证 Rx 卓越的处理能力。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |