angular的表单分响应式表单和模板驱动表单。
响应式表单比模板驱动表单更有可伸缩性。它们提供对底层表单 API 的直接访问,并且在视图和数据模型之间使用同步数据流,从而可以更轻松地创建大型表单。
模板驱动表单专注于简单的场景,可复用性没那么高。在视图和数据模型之间使用异步数据流。
FormControl
实例用于追踪单个表单控件的值和验证状态。FormGroup
用于追踪一个表单控件组的值和状态。FormArray
用于追踪表单控件数组的值和状态。ControlValueAccessor
用于在 Angular 的 FormControl
实例和内置 DOM 元素之间创建一个桥梁。对于响应式表单,你可以直接在组件类中定义表单模型。[formControl]
指令会通过内部值访问器ControlValueAccessor
来把显式创建的 FormControl
实例与视图中的特定表单元素联系起来。
在下面例子中,表单模型是 FormControl
实例。
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-reactive-favorite-color',
template: `
Favorite Color:
`
})
export class FavoriteColorComponent {
favoriteColorControl = new FormControl('');
}
图 1.在响应式表单中直接访问表单模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jExfEugf-1658319900786)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4af91c701a9445d09ae1ff1a1b1e8066~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]
在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型(FormControl
实例)。 从视图到模型的修改以及从模型到视图的修改都是同步的,而且不依赖于 UI 的渲染方式。
视图=>模型 的数据流步骤:
ControlValueAccessor
会监听表单输入框元素上的事件,并立即把新值传给 FormControl
实例。FormControl
实例会通过 valueChanges
这个可观察对象发出这个新值。valueChanges
的任何一个订阅者都会收到这个新值。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0BiMRrc-1658319900787)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04593707a76544d298844c569a5af0c1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]
模型=>视图 的数据流步骤:
favoriteColorControl.setValue()
方法被调用,它会更新这个 FormControl
的值。FormControl
实例会通过 valueChanges
这个可观察对象发出新值。valueChanges
的任何订阅者都会收到这个新值。ControlValueAccessor
会把控件更新为这个新值。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7XwO49i-1658319900787)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8ad976a94cb8432cacf18f49e9297276~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]
响应式表单将formControl
实例挂载到formControl
指令或者formControlName
指令上,两种指令再通过内部的值访问器ControlValueAccessor
把FormControl
实例与视图中的特定表单元素联系起来。
Angular 为所有原生 DOM 表单元素创建了 Angular
表单控件
Accessor | Form Element |
---|---|
DefaultValueAccessor | input,textarea |
CheckboxControlValueAccessor | input[type=checkbox] |
NumberValueAccessor | input[type=number] |
RadioControlValueAccessor | input[type=radio] |
RangeValueAccessor | input[type=range] |
SelectControlValueAccessor | select |
SelectMultipleControlValueAccessor | select[multiple] |
从上表中可看到,当 Angular 在组件模板中中遇到 input
或 textarea
DOM 原生控件时,会使用DefaultValueAccessor
指令。
formControl
指令
实例化时,初始化ControlValueAccessor
,调用 setUpControl()
函数
// form_control_directive.ts
export class FormControlDirective extends NgControl implements OnChanges {
...
constructor(
...
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
) {
...
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
/** @nodoc */
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);
....
}
}
}
formControlName
指令
实例化时,初始化ControlValueAccessor
,调用formGroup
指令的addControl()
, addControl
方法中再调用setUpControl()
函数。
// form_control_name.ts
export class FormControlName extends NgControl implements OnChanges, OnDestroy {
...
constructor(
...
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],) {
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
/** @nodoc */
ngOnChanges(changes: SimpleChanges) {
if (!this._added) this._setUpControl();
...
}
private _setUpControl() {
...
// 调用formGroup指令里的addControl()
(this as {control: FormControl}).control = this.formDirective.addControl(this);
...
this._added = true;
}
}
formGroup
指令
// form_group_directive.ts
export class FormGroupDirective ... {
...
/**
* @description
* Method that sets up the control directive in this group, re-calculates its value
* and validity, and adds the instance to the internal list of directives.
*
* @param dir The `FormControlName` directive instance.
*/
addControl(dir: FormControlName): FormControl {
...
setUpControl(ctrl, dir);
...
return ctrl;
}
}
setUpControl()
为formControl实例注册事件监听,实现原生表单控件和 Angular 表单控件的数据同步。
//shared.ts
// 为formControl实例注册事件监听
export function setUpControl(control: FormControl, dir: NgControl): void {
...
// 调用 writeValue() 初始化视图表单控件值
dir.valueAccessor!.writeValue(control.value);
// 注册视图改变的监听事件
setUpViewChangePipeline(control, dir);
// 注册表单控件值更新监听事件
setUpModelChangePipeline(control, dir);
// 注册视图失焦事件
setUpBlurPipeline(control, dir);
}
// 原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新 视图 => 模型
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor!.registerOnChange((newValue: any) => {
...
if (control.updateOn === 'change') updateControl(control, dir);
});
}
// 原生控件失焦,Angular 表单控件值也更新 视图 => 模型
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor!.registerOnTouched(() => {
...
if (control.updateOn === 'blur' && control._pendingChange) updateControl(control, dir);
});
}
// 更新formcontrol实例值
function updateControl(control: FormControl, dir: NgControl): void {
...
control.setValue(control._pendingValue, {emitModelToViewChange: false});
}
// 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新 模型 => 视图
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor!.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
}
FormControl实例
export class FormControl extends AbstractControl {
// 控件值改变事件
_onChange: Function[] = [];
// 更新控件值
setValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
(this as {value: any}).value = this._pendingValue = value;
if (this._onChange.length && options.emitModelToViewChange !== false) {
this._onChange.forEach(
(changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
}
// 更新值和校验
this.updateValueAndValidity(options);
}
/**
* Register a listener for change events.
*
* @param fn The method that is called when the value changes
*/
registerOnChange(fn: Function): void {
this._onChange.push(fn);
}
}
/**
*重新计算控件的值和验证状态。
*
*默认情况下,它还更新其祖先的值和有效性。
*
* @param opts配置选项确定控件传播更改和发出事件的方式
*应用更新和有效性检查后。
* * `onlyself`:为true时,仅更新此控件。当为假或未提供时,
*更新所有直接祖先。默认值为false。
* * `emitEvent`:当提供true或未提供(默认值)时,`statusChanges`和`值更改'
*
*当控件更新时,可观察对象会发出具有最新状态和值的事件。
*为false时,不发出任何事件。
*/
updateValueAndValidity(
opts: {onlySelf?: boolean,
emitEvent?: boolean} = {}
): void {
this._setInitialStatus();
this._updateValue(); // 更新value,如果外面调用disable(),会从value中去掉该项
if (this.enabled) {
this._cancelExistingSubscription();
(this as {errors: ValidationErrors | null}).errors = this._runValidator(); // 生成校验信息
(this as {status: string}).status = this._calculateStatus();
if (this.status === VALID || this.status === PENDING) {
this._runAsyncValidator(opts.emitEvent);
}
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter).emit(this.value); // 发出新值
(this.statusChanges as EventEmitter).emit(this.status);
}
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts); // 重新计算formGroup的值和验证状态。
}
}
通过源码分析,angular响应式表单实现原理是:
表单控件formControl实例化的时候,也就是formControlName指令初始化的时候,执行了两个操作:
一,调用formControl实例的registerOnChange()函数,将值访问器ControlValueAccessor更新表单DOM 视图的方法writeValue()注册在formControl实例的_onChange事件列表中;
二,调用值访问器ControlValueAccessor的registerOnChange()方法,将formControl实例更新表单模型值 的方法setValue()与值访问器的onChange()绑定;
表单DOM值改变 => 触发值访问器onChange() => 触发控件setValue() => 更新表单控件值
表单控件值改变 => 遍历控件_onChange => 触发值访问器writeValue() => 更新表单DOM值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GmMDgtUM-1658319900787)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1a07227405944d539ad1db30cff3e6d0~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
视图 => 模型:
input输入改变,触发ControlValueAccessor值访问器onChange()
方法,在钩子函数registerOnChange()
中,onChange()
与回调函数fn()
绑定,fn()
是指令实例化的时候调用setUpControl()
函数注册事件时候的回调。fn()
调用updateControl()
,updateControl()
中会执行control.setValue()
从而更新FormControl
实例的值。
模型 => 视图:
control.setValue()
更新表单控件值,然后遍历control.registerOnChange()
注册的事件列表_onChange
,该事件列表中注册了值访问器的writeValue()
钩子,执行writeValue()
就会更新DOM控件的值。
FormGroup
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl(''),
zip: new FormControl('')
})
});
}
FormArray
适用于创建动态表单,管理任意数量的匿名控件。不需要为每个控件定义一个名字作为 key,因此,如果事先不知道子控件的数量,可选择FormArray创建表单。
定义 FormArray 控件
你可以通过把一组(从零项到多项)控件定义在一个数组中来初始化一个 FormArray
。
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
aliases: this.fb.array([
this.fb.control('')
])
});
FormGroup
中的这个 aliases
控件现在管理着一个控件,将来还可以动态添加多个。
访问 FormArray 控件
通过 getter 来访问控件很方便,这种方法还能很容易地重复处理更多控件。
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
动态添加控件
addAlias() {
this.aliases.push(this.fb.control(''));
}
FormBuilder
FormBuilder
服务有三个方法:control()
、group()
和 array()
。这些方法都是工厂方法,用于在组件类中分别生成 FormControl
、FormGroup
和 FormArray
。
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
@Component({
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
profileForm = this.fb.group({
firstName: [''],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
});
constructor(private fb: FormBuilder) { }
}
ngOnInit(): void {
this.heroForm = new FormGroup({
name: new FormControl(this.hero.name, [
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
});
}
get name() { return this.heroForm.get('name'); }
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {forbiddenName: {value: control.value}} : null;
};
}
创建表单模型时,把一个新的验证器传给FormGroup的第二个参数。
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
}, { validators: identityRevealedValidator });
export const identityRevealedValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const name = control.get('name');
const alterEgo = control.get('alterEgo');
return name && alterEgo && name.value === alterEgo.value ? { identityRevealed: true } : null;
};
封装表单控件的有什么好处?
1、表单是由各种控件组合在一起的,封装表单控件,可以将复杂的表单拆解为不同的控件,表单需要什么控件就引入相应的控件,这样表单功能容易扩充,在业务多变性的情况下,表单控件可以让表单更灵活。
2、表单控件是一个单独的组件,可复用;表单控件可以是一个表单项,还可以是一个表单,将复杂的表单拆解为控件,有利于开发和维护。
封装表单控件注意事项
1、必须为表单控件提供值访问器ControlValueAccessor。必须将表单控件加入到验证器集合NG_VALIDATORS,这样控件的校验才会绑定到表单校验。
2、必须实现ControlValueAccessor类和Validator接口。
例:封装的组件(表单控件missionControl)是一个表单
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
@Component({
selector: 'app-mpi-mode-control',
templateUrl: './mpi-mode-control.component.html',
styleUrls: ['./mpi-mode-control.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MpiModeControlComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MpiModeControlComponent),
multi: true,
},
],
})
// 需要实现ControlValueAccessor, Validator
export class MpiModeControlComponent implements OnInit,
ControlValueAccessor, Validator {
formGroup: FormGroup;
private propagateChange = (_: any) => {};
private propagateTunched = (_: any) => {};
constructor(
private fb: FormBuilder,
private customValidatorsService: CustomValidatorsService
) {
this.formGroupConfig();
this.getFormGroupState();
}
// 更新视图
writeValue(mpiRunFormData: TMpiRunFormInfo) {
...
this.formGroup.patchValue(mpiRunFormData);
}
// 视图控件change事件,更新表单控件值
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
// 视图控件blue事件,更新表单控件值
registerOnTouched(fn: any): void {
this.propagateTunched = fn;
}
// 将控件校验添加到表单校验
validate(control: AbstractControl): ValidationErrors {
return this.formGroup?.valid
? null
: { missionHpcCreateControl: { valid: false } }; // 可以为任意对象,比如{ valid:false },返回值为control.errors,详见源码updateValueAndValidity()方法
}
/**
* 设置响应式表单
*/
private formGroupConfig() {
this.formGroup = this.fb.group(
{
mpiOnly: [false],
shareDirectory: [
'',
[
this.customValidatorsService.checkEmpty(),
this.customValidatorsService.pathValidator()
],
],
systemPerformance: this.fb.group({
system: [false],
}),
},
{ validators: this.textValidator() }
);
}
// 获取表单状态
private getFormGroupState() {
this.formGroup.valueChanges.subscribe((valuesAndVaild) => {
...
this.propagateChange(valuesAndVaild);
});
}
}