表单
angular 中提供了两种表单形式:响应式表单和模板驱动表单。
理论准备
选择适合你的表单形式:
- 响应式表单提供对底层表单对象模型直接、显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。适用于比较复杂的表单。
- 模板驱动表单依赖模板中的指令来创建和操作底层的对象模型。它们很容易添加到应用中,但在扩展性方面不如响应式表单。适用于简单的表单。
无论哪种形式的表单,都会有下面4个常用基础类:
FormControl
实例用于追踪单个表单控件的值和验证状态;FormGroup
用于追踪一个表单控件组的值和状态;FormArray
用于追踪表单控件数组的值和状态;ControlValueAccessor
用于在Angular
的FormControl
实例和原生 DOM 元素之间创建一个桥梁。
响应式表单
按照实际开发的操作程序,我们会单独创建一个模块来演示:
ng g m forms-study/forms-study --flat
将模块引入到你想要放的模块中。
再创建一个今天练习的组件并调用:
ng g c forms-study/reactive-forms -s -c OnPush
一切准备就绪后,我们来创建一个最简单的响应式表单:
- 注册响应式表单模块,该模块声明了一些用在响应式表单中的指令:
// forms-study.module.ts
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
// ...
ReactiveFormsModule
],
})
- 生成一个
FormControl
实例,并把它保存在组件中:
// reactive-forms.component.ts
export class ReactiveFormsComponent implements OnInit {
name = new FormControl(''); // 默认值为''
}
3.在模板中使用 FormControl
:
<!-- reactive-forms.component.html -->
<label>
Name: <input type="text" [formControl]="name">
</label>
这样,我们就创建了一个最简单的响应式表单。
使用 value
获取表单的值:
<p>value: {{name.value}}</p>
表单分组
FormGroup
的实例跟踪一组 FormControl
实例(比如一个表单)的表单状态。当创建 FormGroup
时,其中的每个控件都会根据其名字进行跟踪。
使用步骤跟上面类似:
- 创建一个
FormGroup
实例:
// reactive-forms.component.ts
export class ReactiveFormsComponent implements OnInit {
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
});
}
- 把这个
FormGroup
模型关联到视图。
<!-- reactive-forms.component.html -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label>
First Name: <input type="text" formControlName="firstName" class="form-control form-control-sm">
</label>
</div>
<div class="form-group">
<label>
Last Name: <input type="text" formControlName="lastName" class="form-control form-control-sm">
</label>
</div>
<button class="btn btn-primary btn-sm" type="submit">提交</button>
</form>
- 保存表单数据。
// reactive-forms.component.ts
onSubmit(): void {
console.log(this.profileForm.value); // {firstName: "abc", lastName: "def"}
}
嵌套表单组
表单组可以同时接受单个表单控件实例和其它表单组实例作为其子控件。这可以让复杂的表单模型更容易维护,并在逻辑上把它们分组到一起。
比如我们增加一个 address
信息:
// reactive-forms.component.ts
export class ReactiveFormsComponent implements OnInit {
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({ // 新增,并设置默认值
street: new FormControl('建设路32号'),
city: new FormControl('成都市')
})
});
}
当我们使用嵌套的表单组时,我们模板对应的 html
代码也应该具有相同的结构,并且设置 FormGroupName
值为表单组名:
<!-- reactive-forms.component.html -->
<!-- 其他代码 -->
<div formGroupName="address">
<div class="form-group">
<label>
street: <input type="text" formControlName="street" class="form-control form-control-sm">
</label>
</div>
<div class="form-group">
<label>
city: <input type="text" formControlName="city" class="form-control form-control-sm">
</label>
</div>
</div>
tips:如果不提供 FormGroupName
参数,在下面的 formControlName
也不能使用 address.street
。
获取到的数据也是嵌套形式:
更新表单数据
更新表单数据有两种方式:
- 使用
setValue()
方法来为单个控件设置新值。setValue()
方法会严格遵循表单组的结构,并整体性替换控件的值。 - 使用
patchValue()
方法可以用对象中所定义的任何属性为表单模型进行替换。
我们添加两个方法来演示 setValue
及 patchValue
两个方法来修改 lastName
:
// reactive-forms.component.ts
onSetValue(): void {
this.profileForm.setValue({
address: {
city: '北京市'
}
});
}
onPatchValue(): void {
this.profileForm.patchValue({
address: {
city: '北京市'
}
});
}
patchValue()
方法修改成功,而 setValue()
方法则报错了:
因为 setValue()
方法严格遵循表单组的结构,所以需要将所有的表单控件的值都加上才能修改成功。
得出结论:修改表单部分值的时候使用 patchValue()
方法,否则使用 setValue()
方法。
使用 FormBuilder 服务生成控件
当表单中有很多控件时,为每个控件实例化 FormControl
显得很麻烦。FormBuilder
服务提供了一些便捷方法来生成表单控件。
class FormBuilder {
group(controlsConfig: { [key: string]: any; }, options: AbstractControlOptions | { [key: string]: any; } = null): FormGroup
control(formState: any, validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormControl
array(controlsConfig: any[], validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormArray
}
FormBuilder
有三个方法:
group()
构建一个新的FormGroup
实例。control()
构建一个新的FormControl
实例。array()
构造一个新的FormArray
实例。
使用 FormBuilder
的 group()
方法修改上面的表单创建形式:
// reactive-forms.component.ts
// 注入服务
constructor(private fb: FormBuilder) { }
// 构建表单
profileForm = this.fb.group({
firstName: [''],
lastName: [''],
address: this.fb.group({
street: ['建设路32号'],
city: ['成都市']
})
});
这样创建是不是清爽很多呢?效果完全一样。
创建动态表单
有的时候,我们不知道要创建的表单具体有多少个。比如给一个商品增加 size
属性,我们根本不知道它有几个尺寸,所以就需要动态创建表单。
使用 FormBuilder.array()
方法来定义 FormArray
实例数组:
// reactive-forms.component.ts
profileForm = this.fb.group({
...
sizes: this.fb.array([
this.fb.control('')
])
});
使用 getter
语法创建类属性 sizes
,以便我们在模板中使用:
// reactive-forms.component.ts
import {FormArray} from '@angular/forms';
get sizes() {
return this.profileForm.get('sizes') as FormArray; // 断言为FormArray类型
}
自定义 addSize()
方法,用 FormArray.push()
方法为 sizes
控件添加新属性:
// reactive-forms.component.ts
import {FormArray} from '@angular/forms';
addSize(): void {
this.sizes.push(this.fb.control(''));
}
模板中渲染 sizes
:
<!-- reactive-forms.component.html -->
<!-- 其他代码 -->
<div formArrayName="sizes">
<div class="form-group" *ngFor="let size of sizes.controls; let i = index">
<label>
Size:
<input type="text" [formControlName]="i" class="form-control form-control-sm">
</label>
</div>
</div>
tips:
- 跟
FormGroup
一样,也需要先提供一个formArrayName
属性; - 通过
sizes.controls
获取添加的动态控件。
获取数据是一个数组的形式:
模板驱动表单
我们也将创建一个 template-forms
组件来演示:
ng g c forms-study/template-forms -s -c OnPush
所谓模板驱动表单,通俗的讲就是利用 ngModel
等指令实现的表单。
要使用 ngModel
指令,必须要做的第一件事就是引入 FormsModule
:
// forms-study.module.ts
@NgModule({
imports: [
FormsModule,
...
]
})
接下来,我们将通过模板驱动表单的形式创建前面的表单。
- 构建数据
为了使我们的数据有一个严谨的结构,我们使用一个类来定义数据格式:
// template-forms.component.ts
export class Person {
constructor(
public firstName: string,
public lastName: string,
public street: string,
public city: string,
) {
}
}
实例化数据并设置默认值:
// template-forms.component.ts
export class TemplateFormsComponent implements OnInit {
model = new Person('', '', '建设路32号', '成都市');
}
- 在模板中绑定数据:
<!-- template-forms.component.html -->
<form>
<div class="form-group">
<label>
First Name: <input type="text" [(ngModel)]="model.firstName" name="firstName" class="form-control form-control-sm">
</label>
</div>
<div class="form-group">
<label>
Last Name: <input type="text" [(ngModel)]="model.lastName" name="lastName" class="form-control form-control-sm">
</label>
</div>
<div class="form-group">
<label>
street: <input type="text" [(ngModel)]="model.street" name="street" class="form-control form-control-sm">
</label>
</div>
<div class="form-group">
<label>
city: <input type="text" [(ngModel)]="model.city" name="city" class="form-control form-control-sm">
</label>
</div>
<div class="btn-group">
<button class="btn btn-primary btn-sm" type="submit">提交</button>
</div>
</form>
注意:如果要在 <form>
标签中使用 ngModel
指令,应该提供 name
属性(详见2.3 双向绑定)。这就相当于响应式模板中提供的 formControlName
属性。
这样,我们实现了数据的双向绑定,也仅仅是双向绑定。如果想用表单的方法,就跟上面一样,还需要在 <form>
标签上提供一个类似 FormGroup
的属性:
<!-- template-forms.component.html -->
<form #profileForm="ngForm">
...
</form>
如果引入了 FormsModule
, <form>
标签上就可以使用 ngForm
指令。 这里的 profileForm
就是 ngForm
类型, profileForm.form
就是 FormGroup
实例。
将表单自己作为参数传入:
<!-- template-forms.component.html -->
<form #profileForm="ngForm" (ngSubmit)="onSubmit(profileForm)">
...
</form>
通过 FormGroup
实例拿到表单数据:
// template-forms.component.ts
onSubmit(f: NgForm): void {
console.log(f.form.value); // {firstName: "aa", lastName: "bb", street: "建设路32号", city: "成都市"}
}
使用 FormGroup
的 reset()
方法重置表单:
// template-forms.component.ts
resetForm(f: NgForm): void {
// reset(value: any = {}, options: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
f.reset({firstName: 'Jack', lastName: 'Bob'});
}
跟踪控件状态
Angular
会跟踪表单组件的状态,会告诉你用户是否接触过该控件、该值是否发生了变化,或者该值是否无效。并且会在控件上以特殊的 CSS
类来反映其状态。
我们可以用这些 CSS
类来根据控件的状态定义其样式。这也将为后面我们介绍表单验证做一点准备。
总结
- 响应式表单主要是通过操作表单对象来定义表单,用于较复杂结构的表单,模版驱动表单则相反;
- 修改表单部分值的时候使用
patchValue()
方法,否则使用setValue()
方法; - 使用
FormArray.push()
方法可以创建动态表单; - 可以根据
angular
跟踪的表单状态来定制样式。
欢迎关注我的公众号,公众号将第一时间更新angular教程: