Angular Form Part1

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 的定义来自于一个对象,每个节点的数据点就是这个对象的一个属性。再直接一点就是,它将一个对象映射到一个 Form 表单上。对象的结构原则上是不变的。
    • Template-Drive: 一个 ngModel 就是一个数据点,每个数据点之间是独立的,可以很方便的添加删除。
  • 验证
    • Reactive: 由于我们拥有FormControl,FormGroup,可以很容易的通过函数调用的方式对表单进行验证。
    • Template-Drive: 则需要我们写 directive,来进行验证。

我们先来看一下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
  • 父子组件操作的 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里面(异步),ngModelngOnChanges会发现变化,它会将该值赋值给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()
  • 最后一个,Formsubmit,reset方法也被它给接管了。
原文地址:https://www.cnblogs.com/kongshu-612/p/15429698.html