Angular Form
Reactive Form vs Template-Driven
- Reactive Form 需要我们自己创建
FormControl,FormGroup,FormArray
的对象,然后将FormControl
绑定到 html 上的 Form 上,FormControl
绑定到 html 上的控件上。 - Template-Driven 则是通过
[(ngModel)]=xxx
这种方式来将控件与数据关联。它使用起来简单,但是不易扩展,功能单一。
区别: - 数据流
- Reactive Form: model 与 Dom 的数据刷新是同步的,Dom 事件
onValueChange
被 Form 接管,立刻写入 FormControl 中,FormControl 的 setVaule 方法,会通过dom.value=xxx
立刻将值写入 Dom。因为它内部保留了 Dom 的引用。 - Template-Driven: Dom 到 model 的刷新是通过
onValueChange
,同样也是同步的。model 到 Dom 的刷新则是异步的。model 反应到ngModel
则是需要changeDetect
,这一步则是异步的。
- Reactive Form: model 与 Dom 的数据刷新是同步的,Dom 事件
- 数据模型
- Reactive: 数据结构化,数据结构不易变,直白点就是,Form 的定义来自于一个对象,每个节点的数据点就是这个对象的一个属性。再直接一点就是,它将一个对象映射到一个 Form 表单上。对象的结构原则上是不变的。
- Template-Drive: 一个 ngModel 就是一个数据点,每个数据点之间是独立的,可以很方便的添加删除。
- 验证
- Reactive: 由于我们拥有
FormControl,FormGroup
,可以很容易的通过函数调用的方式对表单进行验证。 - Template-Drive: 则需要我们写 directive,来进行验证。
- Reactive: 由于我们拥有
我们先来看一下FormControl
的一些方法,大家可以参考form.d.ts
其实里面说的很清楚,来了解它的一些特性。
先了解一下三个主要的类型。
- FormControl: 一般是用来关联某个控件或者 input,它就是我们要拿到的值。可以理解成 DomElement 对于 Angular 的抽象。
- FormGroup: 这个用来特指一个组合,一个对象,一般我们会将它跟 Form 关联,可以理解成带有{}的东西。
- FormArray: 这个用来指数组,它里面的元素可以是一个 FormControl,或者是 FormGroup。
- 组件的定位有两种格式
['grandparent','parent','self','child']
,'grandparent.parent.self.child'
这三个类型都继承了AbstractControl
,所以,我们先了解一下AbstractControl
,这个里面定义了Form最主要的功能。
-
value 相关的一些属性与方法,首先 Form 中最关键的问题是 Value, 双向数据流同步组件的值。
- value:any,
- setValue(value:any,options?:any),
- patchValue(value:any,options?:any),是为 FormControl 准备的,它是只更新 Form 数据中结构与 value 结构相同的值,对其他的值保持不变。对于 FormControl 与 setValue 行为一致。对于 FormArray,它只更新重叠的属性。
- resetValue(value:any,options?:any),它除了可以设置值,还可以设置 Control status,取决于,value 的值,与 patchValue 类似,区别是可以传递{value:xxx,disabled:true}带有状态。
valueChanges: Observable<any>
: 每次值变化的时候,都会发事件,要么编程改变值,要么 Dom 事件促发的改变,enable()/disable() 也会触发,除非传递参数湮灭这个事件。
-
status 相关的属性与方法。
- status: 一般有这几个 VALID INVALID PENDING DISABLED 这四个状态。
- get valid():boolean, get invalid():boolean, get pending():boolean, get disabled():boolean, get enabled():boolean,
pristine, get dirty():boolean, touched, get untouched():boolean, statusChanges:Observable<any>
statusChange: 每次 status 重新计算都会 emit 这个事件。pristine:原始的。get updateOn():FormHooks
: 获取它是通过哪个事件来或触发去获取控件的值的,有效值为:change|blur|submit
,默认是change
。- enable() , disable(),
- markAsTouched(),markAllAsTouched(),markAsUntouched(),markAsDirty(),markAsPristine(),markAsPending(), 这些都很直接。
- updateValueAndValidity(): 这个很直接,看名字就知道干啥了。
-
验证与错误
- error:
ValidationErrors:{[key:string]:string}
,标记验证错误的结果,key 一般是 ruleName, value 一般是 errorMessage. validator:ValidatorFn
,我们一般会定义很多 validations, Form 内部会被封装到这个 validator 内部,调用这个内部会按顺序来验证,结果保存到error里面。
declare interface ValidatorFn { (control: AbstractControl): ValidationErrors | null;
asyncValidator: AsyncValidatorFn | null;
跟上面功能类似,注意两点,详情- 每个异步验证方法会返回一个
Promise<ValidationErrors>|Observables<ValidationErrors>
,最终的结果会是Fork.join(asyncValidator[])
,所以,我们提供的 asyncValidaor,一定要返回一个 Promise 结果,真正的 Promise, 如果是 Observable, 记住一定要让它状态变成completed. - 异步验证只有同步验证通过了,才会触发,否则,如果同步验证挂了,异步验证压根不会执行。
- 操作 validator 的 API,要想让新的 validators 生效,必须调用
updateValueAndValidity()
setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void; setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null): void; clearValidators(): void; clearAsyncValidators(): void;
getError(errorCode: string, path?: Array<string | number> | string): any;
获取某个子 Control 上某个 rule 的错误内容setErrors(errors:ValidationErrors):void
hasError(errorCode: string, path?: Array<string | number> | string): boolean
- error:
-
父子组件操作的 API.
get parent():FormGroup|FormArray;
获取当前 Control 的父级。setParent(parent:FormGroup|FormArray)
.get(path:Array<string|number>|string):AbstractControl|null
获取某个路径下的 Control,路径有两种方式:数组方式,或者点分的字符串。get root():AbstractControl
获取顶级的 Contorl。
接下来了解一下AbstractControlDirective ControlContainer NgControl这三个类型,第一个是基类,后面的两种是它的实现。
AbstractControlDirective
它的功能是提供对于AbstractControl
属性的ReadOnly的访问,所以它的所有方法都是get
。
- 值相关的,
get value():any,
get control():AbstractControl|null
- 状态相关的
get valid():boolean|null, get invalid():boolean|null
get pending():boolean|null, get disabled():boolean|null, get enabled():boolean|null
- ...
- 值状态事件相关的
get valueChanges():Observable<any>|null;
get statusChanges(): Observable<any> | null;
- 唯一的操作 API
reset(value?: any): void;
ControlContainer
这个可以理解成是对于FormGroup
的封装实现,提供访问FormGroup
属性的 API。
name:string|number|null
get formDirective():Form|null
NgControl
这个可以理解成对FormControl
的封装,提供只读FormControl
属性的 API。NgModel
是这个类的实现。这个就是对应于FormControl。
valueAccessor: ControlValueAccessor | null;
ControlValueAccessor
这个是 Angular Form API 与 Dom 之间的桥梁,数据流从 Angular 到 DOM, 以及从 DOM 到 Angular 都是通过这个类实现的。它的定义如下,其实就是个接口。
简单说明一下,我们自己定义的组件,只要实现了这个接口,对于 Angular(更确切的说就是 NgModel 或者 NgControl) 而言,它就是一个潜在的FormControlElement,NgControl.valueAccessor
,如上所定义。Angular 是看不到 Dom 的,它看到的就是ControlValueAccessor
这个抽象。当数据流从 Angular 到外部(比如 Dom),那么 Angular 会调用ControlValueAccessor.writeValue(value)
这样外部就拿到值了。而在初始化的时候,Angular 会先把回调方法通过ControlValueAccessor.registerOnChange(callbackFn)
,注册到 Angular 外部(比如 Dom),那么当外部值变化时,外部会主动调用callbackFn
,这样 Angular 就拿到值了,
interface ControlValueAccessor {
// Angular 到DOM
writeValue(obj: any): void;
// Dom到Angular
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
abstract viewToModelUpdate(newValue: any): void;
这个是个新方法,未知
NgModel
这是我们经常用的[(NgModel)]=xxx
,常用于双向绑定。它是 Template-driven 的关键词,是一个Directive。其中最重要的两个概念就是FormControl,ControlValueAccessor。
-
FormControl,可以理解成 Angular 内部的抽象,进一步提供对业务组件的访问。
-
ControlValueAccessor,可以理解成数据源,或者 DOM 封装
-
那么NgModel的作用就是沟通我们的客户段业务组件与用户的输入组件。它提供了丰富的 API 让我们同时可以操作 FormControl,或者改变 DOM 的值,同时NgModel也是一个 Directive,这边有点绕。
-
NgModel是如何拿到
readonly control: FormControl;
valueAccessor: ControlValueAccessor | null;
,因为我们通常的使用场景就是最简单的<input name='hello' [(NgModel)]='val'/>
control: FormControl
:这个是NgModel自己定义的,这是源码:public override readonly control: FormControl = new FormControl();
- valueAccessor是通过DI拿到的,看构造函数定义.control可能会添加到parent中。而valueAccessor则是直接来自于valueAccessors。
constructor(
@Optional() @Host() parent: ControlContainer,
@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[]) {
super();
this._parent = parent;
this._setValidators(validators);
this._setAsyncValidators(asyncValidators);
this.valueAccessor = selectValueAccessor(this, valueAccessors);
};
- Angular 为我们提供了一个默认的DefaultValueAccessor,代码如下,大部分情况下,我们都是用的这是这个。可以看出来,它接管了
Input
事件。
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);
}
}
我们来看看数据流是怎么流动的。假设我们有如下的 html
<input type="text" [(ngModel)]="val" />
- 当我们在这个文本框输入'H'时,HtMLInputElement 发出 input 事件,该事件会调用注册的方法,该方法时通过
setUpViewChangePipeline()
添加的。这个方法内部会赋值给FormControl
。同时有可能调用updateControl()
方法,在它里面,先调用control.setValue()
,这样,我们的FormControl
就拿到了值。同时会让NgModel
发出(ngModelChange)
事件,这样我们的业务组件val
就拿到了值。 - 如果我们通过改变
val
的值,那么在下一次CD里面(异步),ngModel 的ngOnChanges
会发现变化,它会将该值赋值给FormControl.setValue()
,然后在setUpModelChangePipeline
里面会设置到ControlValueAccessor
上,从而写入 HTMLInputElement 上面。 - 如果我们直接调用
FormControl.setValue()
,那么它会直接进入setUpModelChangePipeline
,更新ControlValueAccessor
,从而更新 HTMLInputElement,FormControl.setValue()
,有个参数决定这个行为要不要发出(ngModelChange)
,让 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************************
NgForm
这个是我们不怎么用的一个组件,但它一直存在。这个是它sourceCode
下面是它源码的片段。得到如下结论
- 为什么说它是
FormControl
背后的默默无闻,看他的selector,只要我们import 'FormsModule'
,那么我们所有的Form就被接管了。 - 它继承了
ControlContainer
,而ControlContainer
又继承了AbstractControlDirective
,它们主要是提供值,status, validation, 以及 Control 的访问,都是读。 - 它实现了
Form
,这个类型的功能主要是对于其内部的FormControl,FormGroup
,add/remove/getxxx的访问。 - 它的构造函数,会拿到Form上定义的 validator,asyncValidator,通过 DI 拿的。它的内部会直接定义最顶层的 Form。
@Directive({
selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]",
providers: [formDirectiveProvider],
host: { "(submit)": "onSubmit($event)", "(reset)": "onReset()" },
outputs: ["ngSubmit"],
exportAs: "ngForm",
})
export class NgForm extends ControlContainer implements Form, AfterViewInit {
constructor(
@Optional()
@Self()
@Inject(NG_VALIDATORS)
validators: (Validator | ValidatorFn)[],
@Optional()
@Self()
@Inject(NG_ASYNC_VALIDATORS)
asyncValidators: (AsyncValidator | AsyncValidatorFn)[]
) {
super();
this.form = new FormGroup(
{},
composeValidators(validators),
composeAsyncValidators(asyncValidators)
);
}
@Input("ngFormOptions") options!: { updateOn?: FormHooks };
form: FormGroup;
ngSubmit = new EventEmitter();
}
第一个问题,它是什么,能干啥?
它是FormGroup
最顶层的引用,本质上也是一个FormGroup
, 它提供了对于整个Form的一种全局访问,包括值,status, validators。
- 我们可以通过
NgForm.form:FormGroup
拿到最顶层的的FormGroup,这就是为什么说它本质上就是一个FormGroup
, - 它可以操作它孩子级别的
FormGroup,Control
,方法名字例如get/add/removexxx
- 值相关的,
updateModel,setValue,onSubmit()
- 最后一个,Form的
submit,reset
方法也被它给接管了。