• 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方法也被它给接管了。
  • 相关阅读:
    怎么使用 Jupyter Notebook 进入指定的目录
    安卓手机安装Xposed框架/钉钉虚拟定位
    Some of the Example google dorks 2019谷歌hacker语法检索表
    在心脏内部,普通的WEB用户会怎样? 心脏滴血漏洞分析学习
    Windows的安全配置基础,打造一个安全点的Windows
    计算机存储单位
    Python如何安装whl文件,python安装py包
    jdk文件中javaw的作用
    msfconsole基础使用,简洁高效学习版
    VirtualBox报错:不能为虚拟电脑XXX打开一个新任务
  • 原文地址:https://www.cnblogs.com/kongshu-612/p/15429698.html
Copyright © 2020-2023  润新知