[Angular 基础] - 表单:响应式表单
之前的笔记:
-
[Angular 基础] - routing 路由(下)
-
[Angular 基础] - Observable
-
[Angular 基础] - 表单:模板驱动表单
开始
其实这里的表单和之前 Template-Driven Forms 没差很多,不过 Template-Driven Forms 主要在 V 层实现,而这里之后的主要功能会在 VM 层实现。
-
V 层代码如下:
<div class="container"> <div class="row"> <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2"> <form> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" class="form-control" /> </div> <div class="form-group"> <label for="email">email</label> <input type="text" id="email" class="form-control" /> </div> <div class="radio" *ngFor="let gender of genders"> <label> <input type="radio" [value]="gender" />{{ gender }} </label> </div> <button class="btn btn-primary" type="submit">Submit</button> </form> </div> </div> </div>
-
VM 层代码
import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent { genders = ['male', 'female']; signupForm: FormGroup; }
-
app module 代码
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, ReactiveFormsModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
⚠️:这里导入的是
ReactiveFormsModule
创建表单
这里会用 Angular 提供的类去实现:
export class AppComponent implements OnInit {
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit(): void {
this.signupForm = new FormGroup({
username: new FormControl(null),
email: new FormControl(null),
gender: new FormControl('male'),
});
}
}
其中 FormGroup
接受的是一个对象,对象中的 key 是当前 FormGroup
需要管理的 from control,value 则是 FormControl
, FormGroup
或 FormArray
sange 三个中的一个
FormControl
中接受的参数则是默认值
到这一步,表单的创建就完成了,下一步需要将表单和 V 层进行同步——此时的 Angular 并不知道 VM 层中的 signupForm
会对 V 层中的表单进行管理,也无法将 ``FormControl` 中的属性与表单中的 input 建立关联
同步 V 和 VM 层
这里依然通过绑定 directive 实现,用到的 directive 包含 formGroup
和 formControlName
,如下:
<form [formGroup]="signupForm">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username"
/>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
[formControlName]="'email'"
/>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input type="radio" [value]="gender" formControlName="gender" />{{ gender
}}
</label>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
其中 formGroup
与 FormGroup
对应,formControlName
与 FormControl
的名字对应,效果如下:
我把 gender 的默认值改成了 femail
,这里可以看到两个 radio button 的状态都是 untouched
,不过默认值被设置成了 female
提交表单 AKA 获取提交的值
这个实现比较简单,可以直接通过属性获取当前表单的值
-
V 层修改
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()"> <!-- 其余不变 --> </form>
-
VM 层修改
onSubmit() { console.log(this.signupForm); }
输出结果如下:
验证
Reactive Form 中的验证通常会通过编程实现,如:
ngOnInit(): void {
this.signupForm = new FormGroup({
username: new FormControl(null, Validators.required),
email: new FormControl(null, [Validators.required, Validators.email]),
gender: new FormControl('female'),
});
}
展示报错信息
这里的报错信息也不是用 directive,而是直接访问 signupForm
:
<p
class="help-block"
*ngIf="
signupForm.get('username').invalid &&
signupForm.get('username').touched
"
>
Please enter a valid username!
</p>
效果如下:
⚠️:CSS 还是可以用一样的方式实现:
input.ng-invalid.ng-touched {
border: 1px solid red;
}
组合表单
这里使用 FormGroup
去解决需要组合数据的情况,比如说地址的组合通常为省+市+具体地址+邮编才能组合成一个完整的地址,实现方法如下:
ngOnInit(): void {
this.signupForm = new FormGroup({
userData: new FormGroup({
username: new FormControl(null, Validators.required),
email: new FormControl(null, [Validators.required, Validators.email]),
}),
gender: new FormControl('female'),
});
}
这个时候 VM 层和 V 层的同步又会失败——这时候 username 和 email 没办法通过 signupForm.attribute
进行获取。也因此,V 层需要进行对应的修改:
<div formGroupName="userData">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
formControlName="username"
/>
<p
class="help-block"
*ngIf="
signupForm.get('userData.username').invalid &&
signupForm.get('userData.username').touched
"
>
Please enter a valid username!
</p>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
class="form-control"
[formControlName]="'email'"
/>
</div>
</div>
这里将 username 和 email 用一个 div formGroupName="userData"
进行绑定,去还原 VM 层的结构。属性的获取也从 signupForm.get('username')
改为了 signupForm.get('userData.username')
通过这样的修改,就可以解决 console 出现的报错信息,当前表单的功能也可以正常运行
Form Array
FormArray
是一个比较方便接受数组数据的结构,实现如下:
-
V 层
<div formArrayName="hobbies"> <h4>Your Hobbies</h4> <button class="btn btn-default" type="button" (click)="onAddHobby()"> Add Hoby </button> <div class="form-group" *ngFor="let hobbieControl of hobbies.controls; let i = index" > <input type="text" class="form-control" [formControlName]="i" /> </div> </div>
-
VM 层
export class AppComponent implements OnInit { ngOnInit(): void { this.signupForm = new FormGroup({ userData: new FormGroup({ username: new FormControl(null, Validators.required), email: new FormControl(null, [Validators.required, Validators.email]), }), gender: new FormControl('female'), hobbies: new FormArray([]), }); } get hobbies() { return this.signupForm.get('hobbies') as FormArray; } onAddHobby() { const control = new FormControl(null, Validators.required); this.controls.push(control); } }
渲染结果如下:
这里 VM 层有两个比较大的变动:
-
getter
getter
主要是为了 TS 验证方便,V 层直接调用signupForm.get('hobbies')
会因为数据类型不明确而导致报错——报错信息大概是这样的:XXX does not exist on AbstractControl<YYY, ZZZ>
看了下,
FormArray
没有 overloadget()
,因此调用的还是AbstractControl
中的函数,所以需要做类型转换用
getter
可以有效的解决这个问题 -
hobbies.controls
FormArray。controls
是 Angular 用来提供循环的值,案例中多用来和ngFor
搭配使用本质上
hobbies
是一个FormArray
,直接在 V 层调用会报错,可以使用push
是因为 Angular 实现了push
:export declare class FormArray< TControl extends AbstractControl<any> = any > extends AbstractControl< ɵTypedOrUntyped<TControl, ɵFormArrayValue<TControl>, any>, ɵTypedOrUntyped<TControl, ɵFormArrayRawValue<TControl>, any> > { push( control: TControl, options?: { emitEvent?: boolean; } ): void; }
自定义验证
本质上来说,validator 就是一个需要传出去的函数,让 Angular 在每次状态变化时调用,因此实现自定义就是实现一个函数,实现如下:
export class AppComponent implements OnInit {
forbiddenUsernames = ['admin', 'super'];
ngOnInit(): void {
this.signupForm = new FormGroup({
userData: new FormGroup({
username: new FormControl(null, [
Validators.required,
this.forbiddenNames.bind(this),
]),
}),
});
}
forbiddenNames(control: FormControl): { [key: string]: boolean } {
if (this.forbiddenUsernames.includes(control.value)) {
return { nameIsForbidden: true };
}
return null;
}
}
效果如下:
👀:注意这里自定义验证传送的方式:this.forbiddenNames.bind(this)
,其原因是因为在 forbiddenNames
调用了 this.forbiddenUsernames
,因此需要绑定对应的 scope,否则 Angular 会因为 this
的指向变更而找不到 this.forbiddenUsernames
。
⚠️:这是 JavaScript 的问题,与框架无关
异步自定义验证
这个实现和自定义验证类似,不过返回的对象是 Promise<any> | Observable<any>
,实现如下:
export class AppComponent implements OnInit {
ngOnInit(): void {
this.signupForm = new FormGroup({
userData: new FormGroup({
email: new FormControl(
null,
[Validators.required, Validators.email],
this.forbiddenEmails
),
}),
});
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((res, rej) => {
setTimeout(() => {
if (control.value === 'admin@test.com') {
res({ emailIsForbidden: true });
} else {
res(null);
}
}, 1500);
});
return promise;
}
}
效果如下:
可以看到,当输入为 admin@test.com
时,又过了大概 1.5s 之后,输入框才跳为红色——这是之前加的出现 error 的 CSS
有一点我截图的时候没截到,就是当等待验证的过程中,Angular 会自动为当前 class 添加一个 ng-pending
的类名:
使用 errors
上面根据自定义验证可以得知,Validators 返回的是一个 { [key: string]: boolean }
,或者准确的说是这个结构:
export declare type ValidationErrors = {
[key: string]: any;
};
export declare interface ValidatorFn {
(control: AbstractControl): ValidationErrors | null;
}
export declare class Validators {
static min(min: number): ValidatorFn;
}
换言之,在 errors
中,要么是 null
,要么应该会返回一个 {[errorName: string]: any}
的类型:
也就是说,如果想要细化报错信息,那么就可以通过 errors
这个对象去实现,V 层修改代码如下:
<p
class="help-block"
*ngIf="
signupForm.get('userData.username').invalid &&
signupForm.get('userData.username').touched
"
>
<span
*ngIf="
signupForm.get('userData.username').errors['nameIsForbidden']
"
>This username is invalid!</span
>
<span *ngIf="signupForm.get('userData.username').errors['required']"
>This field is required!</span
>
</p>
效果展示:
状态追踪
之前提到了 async validator 的使用,这里补充一下怎么追踪类似的变化。这里依旧通过 subscribe 两个 Observable 时实现:
this.signupForm.valueChanges.subscribe((val) => {
console.log(val);
});
this.signupForm.statusChanges.subscribe((val) => {
console.log(val);
});
效果如下:
如果需要 track 状态的变化,从而进行更细致的处理,就可以通过 valueChanges
和 statusChanges
进行
更新数据
这里的用法和 Template-Driven Form 一致,我也就不多提了,具体调用的函数如下:
this.signupForm.setValue({});
this.signupForm.patchValue({});
this.signupForm.reset();
总结
Reactive Form 相对于 Template-Driven Form 灵活性更高一些,不过为了保持 V 层和 VM 层的同步,二者的结构需要保持一致。
V 层中的每一层结构依旧需要使用对应的 directive 与 VM 层中的数据进行绑定:
- 最外层的 form 需要通过
FormGroup
对应formGroup
- form 里的结构都以对应的类型+
Name
的方式绑定,主要包含:FormGroupName
,FormArrayName
和formControlName
Reactive Form 主要功能实现都在 VM 层,操作的对象时 VM 中创建的 FromGroup
对象,对比 Template-Driven Form 是在 V 层自动对表单进行管理,操作时需要将 local reference 传到对应的函数中去,或是使用 @ViewChild
获得对应的 ElementRef
Reactive Form 的 directive 通过 ReactiveFormsModule
引入,而 Template-Driven Form 通过 FormsMorule
Reactive Form | Template-Driven Form | |
---|---|---|
管理 | VM 层 | V 层 |
对应模块 | ReactiveFormsModule | FormsMorule |
绑定方法 | [formGroup]="" , formGroupName="" , etc | local reference #form="ngForm" , #name="ngModel" |
优点 | 控制灵活 | 更少的 boilerplate code |