angular的表单分响应式表单和模板驱动表单。
响应式表单比模板驱动表单更有可伸缩性。它们提供对底层表单 API 的直接访问,并且在视图和数据模型之间使用同步数据流,从而可以更轻松地创建大型表单。
模板驱动表单专注于简单的场景,可复用性没那么高。在视图和数据模型之间使用异步数据流。
1、理解angular响应式表单
常用表单基础类
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: <input type="text" [formControl]="favoriteColorControl">
`
})
export class FavoriteColorComponent {
favoriteColorControl = new FormControl('');
}
图 1.在响应式表单中直接访问表单模型
响应式表单中的数据流
在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型(FormControl
实例)。 从视图到模型的修改以及从模型到视图的修改都是同步的,而且不依赖于 UI 的渲染方式。
视图=>模型 的数据流步骤:
- 最终用户在输入框元素中键入了一个值,这里是 "Blue"。
- 这个输入框元素会发出一个带有最新值的 "input" 事件。
- 这个控件值访问器
ControlValueAccessor
会监听表单输入框元素上的事件,并立即把新值传给FormControl
实例。 FormControl
实例会通过valueChanges
这个可观察对象发出这个新值。valueChanges
的任何一个订阅者都会收到这个新值。
模型=>视图 的数据流步骤:
favoriteColorControl.setValue()
方法被调用,它会更新这个FormControl
的值。FormControl
实例会通过valueChanges
这个可观察对象发出新值。valueChanges
的任何订阅者都会收到这个新值。- 该表单输入框元素上的控件值访问器
ControlValueAccessor
会把控件更新为这个新值。
响应式表单实现原理
响应式表单将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
指令。
1 源码分析
-
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); } }
2 响应式表单原理图
视图 => 模型:
input输入改变,触发ControlValueAccessor值访问器onChange()
方法,在钩子函数registerOnChange()
中,onChange()
与回调函数fn()
绑定,fn()
是指令实例化的时候调用setUpControl()
函数注册事件时候的回调。fn()
调用updateControl()
,updateControl()
中会执行control.setValue()
从而更新FormControl
实例的值。
模型 => 视图:
control.setValue()
更新表单控件值,然后遍历control.registerOnChange()
注册的事件列表_onChange
,该事件列表中注册了值访问器的writeValue()
钩子,执行writeValue()
就会更新DOM控件的值。
2、 如何新建一个表单(FormGroup、FormArray、FormBuilder)
-
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) { } }
3、自定义表单验证器
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;
};
}
4、交叉验证
创建表单模型时,把一个新的验证器传给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;
};
5、如何封装表单控件
-
封装表单控件的有什么好处?
1、表单是由各种控件组合在一起的,封装表单控件,可以将复杂的表单拆解为不同的控件,表单需要什么控件就引入相应的控件,这样表单功能容易扩充,在业务多变性的情况下,表单控件可以让表单更灵活。
2、表单控件可复用,将复杂的表单拆解为控件,有利于开发和维护。
-
封装表单控件注意事项
1、必须为表单控件提供值访问器ControlValueAccessor。必须将表单控件加入到验证器集合NG_VALIDATORS,这样控件的校验才会绑定到表单校验。
2、必须实现ControlValueAccessor类和Validator接口。
例:
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);
});
}
}
<div [formGroup]="formGroup">
<app-mpi-mode-control formControlName="missionControl">
</app-mpi-mode-control>
</div>
参考blog