Angular表单

文章目录

      • 一、[ngModelOptions]="{standalone: true}" 是什么意思?为什么在我们的项目中使用ngModel的时候需要用到?
      • 二、ng表单和表单验证 (https://angular.cn/guide/forms-overview)
        • 2.1 目前Angular中提供的内置验证器有
        • 2.2 创建自定义验证器
        • 2.3 如何将错误信息展示在模板中
      • 三、创建自定义表单组件需要用到ControlValueAccessor,是干嘛的?
        • 3.1 ControlValueAccessor的定义
        • 3.2 理解ControlValueAccessor
        • 3.3 实现自定义表单元素

Shark用得过于熟练,却不大了解的ng表单,在使用shark的过程中产生了以下一些疑问。

一、[ngModelOptions]="{standalone: true}" 是什么意思?为什么在我们的项目中使用ngModel的时候需要用到?

<shark-select  [(ngModel)]="channelId" [data]="channelList">shark-select>

在表单中直接这么写会报错

ChannelListComponent.html:13 ERROR Error: If ngModel is used within a form tag, either the name attribute must be set or the form
control must be defined as ‘standalone’ in ngModelOptions.
Example 1:
Example 2:

再看下ngModelOptions的定义,它其实是ngmodel指定的一个input属性,当standalone值为true时,代表着此 ngModel 不会把自己注册进它的父表单中,其行为就像没在表单中一样。

我们项目中使用ngModel其实是属于Template-driven Form,但shark中其实并没有用到ng的form表单校验。而Angular 会在

标签上自动创建并附加一个 NgForm 指令,NgForm 指令为 form 增补了一些额外特性。 它会控制那些带有 ngModel 指令和 name 属性的元素,监听他们的属性(包括其有效性)。 它还有自己的 valid 属性,这个属性只有在它包含的每个控件都有效时才是真。

forms/src/directives/ng_model.ts:

/**
   * @description
   * Tracks the name bound to the directive. The parent form
   * uses this name as a key to retrieve this control's value.
   */
  // TODO(issue/24571): remove '!'.
  @Input() name !: string;

forms/src/directives/ng_form.ts:

/**
   * @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 `NgModel` directive instance.
   */
  addControl(dir: NgModel): void {
    resolvedPromise.then(() => {
      const container = this._findContainer(dir.path);
      (dir as{control: FormControl}).control =
          container.registerControl(dir.name, dir.control);
      setUpControl(dir.control, dir);
      dir.control.updateValueAndValidity({emitEvent: false});
      this._directives.push(dir);
    });
  }

可以看到Angular 创建了一些 FormControl,并把它们注册到 Angular 附加到 标签上的 NgForm 指令。 注册每个 FormControl 时,使用name属性值作为键值。
所以每个form中的input元素的name值必须是唯一的,或者使用[ngModelOptions]="{standalone: true}"将其排除在表单之外。

那么ng表单和表单验证是什样的?

二、ng表单和表单验证 (https://angular.cn/guide/forms-overview)

ng中一个重要的概念就是表单,ng提供了响应式表单(Reactive Form)和模板驱动表单(Template-driven form)两种形式。这两种形式的表单都基于几个共同的底层概念:

  • FormControl 实例用于追踪单个表单控件的值和验证状态。
  • FormGroup 用于追踪一个表单控件组的值和状态。
  • FormArray 用于追踪表单控件数组的值和状态。
  • ControlValueAccessor 用于在 Angular 的 FormControl 实例和原生 DOM 元素之间创建一个桥梁。

2.1 目前Angular中提供的内置验证器有

class Validators {
  static min(min: number): ValidatorFn
  static max(max: number): ValidatorFn
  static required(control: AbstractControl): ValidationErrors | null
  static requiredTrue(control: AbstractControl): ValidationErrors | null
  static email(control: AbstractControl): ValidationErrors | null
  static minLength(minLength: number): ValidatorFn
  static maxLength(maxLength: number): ValidatorFn
  static pattern(pattern: string | RegExp): ValidatorFn
  static nullValidator(control: AbstractControl): ValidationErrors | null
  static compose(validators: ValidatorFn[]): ValidatorFn | null
  static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
}

添加至响应式表单

this.signupForm = this.fb.group({
  userName: ['', [Validators.required, Validators.minLength(3)]],
  email: ['', [Validators.required, Validators.pattern('[a-z0-9._%+_]+@[a-z0-9.-]+')]]
});

添加至模板驱动式表单


2.2 创建自定义验证器

shared/forbidden-name.directive.ts (forbiddenNameValidator):

/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} | null => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? {'forbiddenName': {value: control.value}} : null;
  };
}

@Directive({
  selector: '[appForbiddenName]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
  @Input('appForbiddenName') forbiddenName: string;

  validate(control: AbstractControl): {[key: string]: any} | null {
    return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
                              : null;
  }
}

添加至响应式表单

this.heroForm = new FormGroup({
  'name': new FormControl(this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i) // <--传入自定义验证器
  ])
})

添加至模板驱动式表单


2.3 如何将错误信息展示在模板中

表单控件有以下6种状态

  • valid - 表单控件有效
  • invalid - 表单控件无效
  • pristine - 表单控件值未改变
  • dirty - 表单控件值已改变
  • touched - 表单控件已被访问过
  • untouched - 表单控件未被访问

可以通过formControl来获取这些属性,这些属性从AbstractControl继承而来。

forms/src/directives/abstract_control_directive.ts

export abstract class AbstractControlDirective {
  get valid(): boolean { return this.control ? this.control.valid : null; }

  get invalid(): boolean { return this.control ? this.control.invalid : null; }

  get errors(): ValidationErrors | null { return this.control ? 
      this.control.errors : null; }

  get pristine(): boolean { return this.control ? this.control.pristine : null; }

  get dirty(): boolean { return this.control ? this.control.dirty : null; }

  get touched(): boolean { return this.control ? this.control.touched : null; }

  get untouched(): boolean { return this.control ? this.control.untouched : null; }

  get valueChanges(): Observable { return this.control ? 
      this.control.valueChanges : null; }

  hasError(errorCode: string, path: string[] = null): boolean {
    return this.control ? this.control.hasError(errorCode, path) : false;
  }

如下,可以在html中获取状态从而显示对应的错误信息

<input type="text" formControlName="foo">

<div *ngIf="form.get('foo').hasError('required') && form.get('foo').touched">
  Field is required
div>
<div *ngIf="form.get('foo').hasError('minlength') && form.get('foo').dirty">
  Min length is 5
div>

附:写这错误显示逻辑未免有些繁琐,可以把其封装成组件,ToddMotto的ngx-error可供参考,更加声明式的表达。

<input type="text" formControlName="foo">

<div ngxErrors="foo">
  <div ngxError="required" when="touched">
    Field is required
  div>
  <div ngxError="minlength" when="dirty">
    Min length is 5
  div>
div>

三、创建自定义表单组件需要用到ControlValueAccessor,是干嘛的?

写自定义表单控件的时候,都需要实现ControlValueAccessor接口,然而仅仅是知道如何实现,却并不知其中含义。

3.1 ControlValueAccessor的定义

ControlValueAccessor是原生元素和Angular表单之间的桥梁,将组件或者指令继承ControlValueAccessor的接口就能当作Angular表单元素使用了。

ControlValueAccessor接口可以帮助我们完成数据和视图的双向转换,它的作用是:

  • Writing a value from the form model into the view/DOM
  • Informing other form directives and controls when the view/DOM changes

Angular 之所以提供这个接口是因为不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的 value 值,而对于复选框 (checkbox) 我们是设置它的 checked 属性。实际上,不同类型的输入控件都有一个 ControlValueAccessor,用来更新视图。

Angular中这些表单元素都实现了ControlValueAccessor接口:

  • CheckboxControlValueAccessor
  • DefaultValueAccessor
  • NumberValueAccessor
  • RadioControlValueAccessor
  • RangeValueAccessor
  • SelectControlValueAccessor
  • SelectMultipleControlValueAccessor

3.2 理解ControlValueAccessor

ControlValueAccessor有这些方法:
forms/src/directives/control_value_accessor.ts

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  setDisabledState(isDisabled: boolean)?: void
}
  • writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中。
  • registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数
  • registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数
  • setDisabledState?(isDisabled: boolean):当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素

举个例子,angular源码中default_value_accessor对writeValue的实现:
writeValue
forms/src/directives/default_value_accessor.ts#L104

/**
   * Sets the "value" property on the input element.
   *
   * @param value The checked value
   */
  writeValue(value: any): void {
    const normalizedValue = value == null ? '' : value;
    this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
  //  <-----这里就是将模型中的新值写入视图或 DOM 属性中
  }

//forms/src/directives/shared.ts#L41

export function setUpControl(control: FormControl, dir: NgControl): void {
  if (!control) _throwError(dir, 'Cannot find control with');
  if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');

  control.validator = Validators.compose([control.validator !, dir.validator]);
  control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
  // **将formControl的值初始化给view**
  dir.valueAccessor !.writeValue(control.value);

  // **监听view的变化,将值同步给formControl和ngModel**
  setUpViewChangePipeline(control, dir);
 //监听formControl,将值同步给view和ngModel
  setUpModelChangePipeline(control, dir);

  setUpBlurPipeline(control, dir);

 //...
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);
  });
}
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;
}
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
  control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
    // control -> view
    dir.valueAccessor !.writeValue(newValue);

    // control -> ngModel
   // **控件值访问器 ControlValueAccessory 还会调用 NgModel.viewToModelUpdate() 方法,它会发出一个 ngModelChange 事件。**
    if (emitModelEvent) dir.viewToModelUpdate(newValue);
  });
}
// .....

如上,在formControl的ngOnChanges中去调用setUpControl,setUpControl又去调用controlValueAccessor的方法。
下图是formControl如何借助于ControlValueAccessor与原生或自定义表单元素交互图

总而言之就是,自定义表单元素通过实现ControlValueAccessor中的writeValue()、registerOnChange()等方法,来实现formControl和DOM之前的双向交互。

3.3 实现自定义表单元素

需要两步:

  1. 注册 NG_VALUE_ACCESSOR provider
  2. 实现 ControlValueAccessor 接口

实现ControlValueAccessor接口上面已经提到过,下面介绍如何注册 NG_VALUE_ACCESSOR提供商。
NG_VALUE_ACCESSOR 提供商用来指定实现了 ControlValueAccessor 接口的类,并且被 Angular 用来和 formControl进行同步。通常是使用该组件类或指令本身来注册。所有表单指令都是使用NG_VALUE_ACCESSOR 作为令牌来注入控件值访问器,然后通过selectValueAccessor方法选择合适的访问器。

export const DEFAULT_VALUE_ACCESSOR: any = {  
provide: NG_VALUE_ACCESSOR,  
useExisting: forwardRef(() => DefaultValueAccessor),  
multi: true
};
})
@Directive({
   ...
  providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {...}

接下来解读下上述代码的含义。
我们使用内置的token NG_VALUE_ACCESSOR来注册 DefaultValueAccessor。
在 TypeScript 里面,类声明的顺序是很重要的。如果一个类尚未定义,就不能引用它。forwardRef() 函数建立一个间接地引用DefaultValueAccessor,使我们在Angular实例化它之前就可以使用它。
useExisting()用来保证只有一个DefaultValueAccessor的实例。
该provider对象有第三个属性 multi: true,把它和DI Tokens一起使用用来为特定事件注册多个处理器,这在当我们想给一个NG_VALUE_ACCESSOR token注册自己的ControlValueAccessors时是非常有用的,把一个token和多个provider关联起来。

参考链接:

  • https://segmentfault.com/a/1190000009037539
  • https://segmentfault.com/a/1190000010064866#articleHeader7
  • https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html
  • https://segmentfault.com/a/1190000009070500#articleHeader1
  • https://blog.angularindepth.com/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms-93b9eee9ee83
  • https://segmentfault.com/a/1190000014129567

你可能感兴趣的:(angular)