Angular Form 总观
先给总结,再谈细节
- ReactForm, Template driven Form 的差异:
- ReactForm: 需要我们自行定义 FormControl,适用于数据结构不变,验证很方便,数据流刷新时同步的。
- Template driven Form: 不需要我们自行定义 FormControl, 适用于数据结构易变,数据流同步,从 ts 到 Dom 是异步的,Dom 到 ts 是同步的。验证需要我们写 Directive.
- 我们可以通过自己实现
ControlValueAccessor
,来实现一个自定义的 Input, 使用的时候更HTMLInputElement类似。 ControlValueAccessor
可以级联。
Form 的两种使用场景与区别
React Form与Template Form,使用的例子如下,这个是官方用例
下面的是三个React Form的使用场景
// React Form
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("");
}
// 这个是一个相对复杂一点的ReactForm 的例子
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
@Component({
selector: "example-app",
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div *ngIf="first.invalid">Name is too short.</div>
<input formControlName="first" placeholder="First name" />
<input formControlName="last" placeholder="Last name" />
<button type="submit">Submit</button>
</form>
<button (click)="setValue()">Set preset value</button>
`,
})
export class SimpleFormGroup {
form = new FormGroup({
first: new FormControl("Nancy", Validators.minLength(2)),
last: new FormControl("Drew"),
});
get first(): any {
return this.form.get("first");
}
onSubmit(): void {
console.log(this.form.value); // {first: 'Nancy', last: 'Drew'}
}
setValue() {
this.form.setValue({ first: "Carson", last: "Drew" });
}
}
// Template Driven Form
import { Component } from "@angular/core";
@Component({
selector: "app-template-favorite-color",
template: `
Favorite Color: <input type="text" [(ngModel)]="favoriteColor" />
`,
})
export class FavoriteColorComponent {
favoriteColor = "";
}
// 复杂一点的Template Driven Form 的例子
import { Component } from "@angular/core";
import { NgForm } from "@angular/forms";
@Component({
selector: "example-app",
template: `
<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
<input name="first" ngModel required #first="ngModel" />
<input name="last" ngModel />
<button>Submit</button>
</form>
<p>First name value: {{ first.value }}</p>
<p>First name valid: {{ first.valid }}</p>
<p>Form value: {{ f.value | json }}</p>
<p>Form valid: {{ f.valid }}</p>
`,
})
export class SimpleFormComp {
onSubmit(f: NgForm) {
console.log(f.value); // { first: '', last: '' }
console.log(f.valid); // false
}
}
这个例子里面遇到几个关键字[formControl]=xxx
,[formGroup]=xxx
,formControlName=xxx
。
NgForm NgModel
。
那么这些个关键字干了啥事,它们是怎么做到的。
- 首先谈谈H5的form功能,以及 Angular Form的功能。
- 获取一个对象的值
{obj1:val1,obj2:val2}
,同时能够支持对每个属性配置 validaiton. - Angular 在此基础上提供了一种基于 API 的操作方式,包括,值,validaiton, 以及对象的结构。所有这些操作不需要操作 DOM,只需要操作
Formxxx
提供的 API, API 接口,主要来自于AbstractFormControl
这个接口
- 获取一个对象的值
- Angular 是如何做到的。这个里面两个数据流,一个是 DOM 到 我们的客户端代码,另一个是从我们的客户端代码触发到 DOM。对于 Angular 而言,它就是个搬运工,一边是 DOM,一边是客户端代码。Angular 对这两个对象做了抽象,虚化出两个类型,DOM 对应于
ControlValueAccessor
,客户端代码则对于与AbstractFormControl
这个接口。formControlDirective
, 以这个为例,我们可以认为这个就是 Angular,它的内部同时拥有FormControl(客户端代码),ControlValueAccessor(DOM),那么它是怎么拿到这两个东西的呢。看源码片段.FormControl来自于Input,ControlValueAccessor来自于 DI,大部分情况下,我们用的是默认的DEFAULT_VALUE_ACCESSOR,源码如下:
// FormControl 源码片段
@Directive({
selector: "[formControl]",
providers: [formControlBinding],
exportAs: "ngForm",
})
export class FormControlDirective
extends NgControl
implements OnChanges, OnDestroy
{
@Input("formControl") form!: FormControl;
constructor(
@Optional()
@Self()
@Inject(NG_VALIDATORS)
validators: (Validator | ValidatorFn)[],
@Optional()
@Self()
@Inject(NG_ASYNC_VALIDATORS)
asyncValidators: (AsyncValidator | AsyncValidatorFn)[],
@Optional()
@Self()
@Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[],
@Optional()
@Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING)
private _ngModelWarningConfig: string | null
) {
super();
this._setValidators(validators);
this._setAsyncValidators(asyncValidators);
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
}
//DEFAULT_VALUE_ACCESSOR 源码
export const DEFAULT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultValueAccessor),
multi: true,
};
@Directive({
selector:
"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]",
// TODO: vsavkin replace the above selector with the one below it once
// https://github.com/angular/angular/issues/3011 is implemented
// selector: '[ngModel],[formControl],[formControlName]',
host: {
"(input)": "$any(this)._handleInput($event.target.value)",
"(blur)": "onTouched()",
"(compositionstart)": "$any(this)._compositionStart()",
"(compositionend)": "$any(this)._compositionEnd($event.target.value)",
},
providers: [DEFAULT_VALUE_ACCESSOR],
})
export class DefaultValueAccessor
extends BaseControlValueAccessor
implements ControlValueAccessor
{
/** Whether the user is creating a composition string (IME events). */
private _composing = false;
constructor(
renderer: Renderer2,
elementRef: ElementRef,
@Optional()
@Inject(COMPOSITION_BUFFER_MODE)
private _compositionMode: boolean
) {
super(renderer, elementRef);
if (this._compositionMode == null) {
this._compositionMode = !_isAndroid();
}
}
/**
* Sets the "value" property on the input element.
* @nodoc
*/
writeValue(value: any): void {
const normalizedValue = value == null ? "" : value;
this.setProperty("value", normalizedValue);
}
/** @internal */
_handleInput(value: any): void {
if (!this._compositionMode || (this._compositionMode && !this._composing)) {
this.onChange(value);
}
}
/** @internal */
_compositionStart(): void {
this._composing = true;
}
/** @internal */
_compositionEnd(value: any): void {
this._composing = false;
this._compositionMode && this.onChange(value);
}
}
-
NgModel,这个的情况有点复杂,相同点,它也有ControlValueAccessor,AbstractFormControl,ControlValueAccessor来自于 DI, AbstractFormControl 是它自己创建的。
-
这两种模式下的数据流的,js 到 DOM 的数据流如下:
- 代码调用
AbstractFormControl.setValue(val)
, - 由于初始化时候,Angular 以及注册了
AbstractFormControl.valueChanges
事件,这个时候,注册的方法会被执行,这个方法里面会调用ControlValueAccessor.writeValue(val)
这样,DOM 就刷新了。
- 代码调用
-
DOM 到 js 的数据流如下,
- Angular 一开始会注册
ControlVAlueAccessor.registerOnChange
,当 DOM 值变化时,会自动调用这个方法,这个方法里面会调用AbstractFormControl.setValue(val)
- 然后会调用 js 到 DOM 的流程,不会出现循环触发的。
- Angular 一开始会注册
-
NgModel 的方式除了上面的流程,还有一个第三点,我们经常使用的场景时
[(ngModel)]=xxx
,也就是说,我们的双向数据流是我们的值也就是ngModel与 DOM 之间的。- 当我们的值改变了,在下一次 CD,ngModel这个组件会发现差异,它会在一个微任务里面调用
AbstractFormControl.setValue()
,然后走上面介绍的流程。 - 当我们的 DOM 发生了改变,也是走上面的流程,我们的
FormControl
会拿到更新的值,同时会调用ngModelChange.emit(val)
,这样我们的代码就拿到了值。
- 当我们的值改变了,在下一次 CD,ngModel这个组件会发现差异,它会在一个微任务里面调用
下面是来自于NgModel的源码片段。
// 注册方法,当ControlValueAccess 值变化,View 该怎么变化, dir: NgModel,
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor!.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
// 这个是FormControl值变化的时候,ControlValueAccessor 的响应。
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
const onChange = (newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor!.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
};
control.registerOnChange(onChange);
// Register a callback function to cleanup onChange handler
// from a control instance when a directive is destroyed.
dir._registerOnDestroy(() => {
control._unregisterOnChange(onChange);
});
}
function updateControl(control: FormControl, dir: NgControl): void {
if (control._pendingDirty) control.markAsDirty();
control.setValue(control._pendingValue, {emitModelToViewChange: false});
dir.viewToModelUpdate(control._pendingValue);
control._pendingChange = false;
}
//**************************NgModel************************
// NgModel 内部的一个方法,主要就是发出(onModelChange)事件
override viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
this.update.emit(newValue);
}
ngOnChanges(changes: SimpleChanges) {
this._checkForErrors();
if (!this._registered) this._setUpControl();
if ('isDisabled' in changes) {
this._updateDisabled(changes);
}
if (isPropertyUpdated(changes, this.viewModel)) {
this._updateValue(this.model);
this.viewModel = this.model;
}
}
// 这时一个微任务,会在CD之后做。
private _updateValue(value: any): void {
resolvedPromise.then(() => {
this.control.setValue(value, {emitViewToModelChange: false});
});
}
//**************************NgModel************************