【问题标题】:Angular reactive form custom control async validationAngular 反应式表单自定义控件异步验证
【发布时间】:2020-04-06 08:48:11
【问题描述】:

更新: 异步验证问题已成功解决。但是初始验证状态还有另一个问题。 查看最新答案。

诀窍如下:

  • 具有实现 ControlValueAccessor 接口的组件用作自定义控件。
  • 此组件在某些反应式表单中用作 FormControl。
  • 此自定义控件具有异步验证器。

问题:

来自 ControlValueAccessor 接口的方法 validate() 在值更改后立即调用,并且不等待异步验证器。当然,控制是无效的和未决的(因为正在进行验证),主窗体也将是无效的和未决的。一切正常。

但是。当异步验证器完成验证并返回 null (意味着值有效)时,自定义控件将有效并且状态也更改为有效,但父级仍然无效并处于挂起状态,因为来自值访问器的 validate() 没有再次调用。

我试图从 validate() 方法返回 observable,但主窗体将其解释为错误对象。

我找到了解决方法:当异步验证器完成验证时,从自定义控件传播更改事件。它迫使主窗体再次调用 validate() 方法并获得正确的有效状态。但它看起来又脏又糙。

问题是: 必须做什么才能使父表单由子自定义控件的异步验证器管理?必须说它与同步验证器配合得很好。

所有项目代码都可以在这里找到: https://stackblitz.com/edit/angular-fdcrbl

主表单模板:

<form [formGroup]="mainForm">
    <child-control formControlName="childControl"></child-control>
</form>

主窗体类:

import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html"
})
export class AppComponent implements OnInit {
  mainForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.mainForm = this.formBuilder.group({
      childControl: this.formBuilder.control("")
    });
  }
}

自定义子控件模板:

<div [formGroup]="childForm">
    <div class="form-group">
        <label translate>Child control: </label>
        <input type="text" formControlName="childControl">
    </div>
</div>

自定义子控件类:

import { Component, OnInit } from "@angular/core";
import { AppValidator } from "../app.validator";
import {
  FormGroup,
  AsyncValidator,
  FormBuilder,
  NG_VALUE_ACCESSOR,
  NG_ASYNC_VALIDATORS,
  ValidationErrors,
  ControlValueAccessor
} from "@angular/forms";
import { Observable } from "rxjs";
import { map, first } from "rxjs/operators";

@Component({
  templateUrl: "./child-control.component.html",
  selector: "child-control",
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: ChildControlComponent,
      multi: true
    },
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: ChildControlComponent,
      multi: true
    }
  ]
})
export class ChildControlComponent
  implements ControlValueAccessor, AsyncValidator, OnInit {
  childForm: FormGroup;

  constructor(
    private formBuilder: FormBuilder,
    private appValidator: AppValidator
  ) {}

  ngOnInit() {
    this.childForm = this.formBuilder.group({
      childControl: this.formBuilder.control(
        "",
        [],
        [this.appValidator.asyncValidation()]
      )
    });
    this.childForm.statusChanges.subscribe(status => {
      console.log("subscribe", status);
    });
  }

  // region CVA
  public onTouched: () => void = () => {};

  writeValue(val: any): void {
    if (!val) {
      return;
    }
    this.childForm.patchValue(val);
  }

  registerOnChange(fn: () => void): void {
    this.childForm.valueChanges.subscribe(fn);
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    isDisabled ? this.childForm.disable() : this.childForm.enable();
  }

  validate(): Observable<ValidationErrors | null> {
    console.log('validate');
    // return this.taxCountriesForm.valid ? null : { invalid: true };
    return this.childForm.statusChanges.pipe(
      map(status => {
        console.log('pipe', status);
        return status == "VALID" ? null : { invalid: true };
      }),
    );
  }
  // endregion
}

【问题讨论】:

  • 您找到解决方案了吗?
  • @AndreiGătej 不,不幸的是问题仍然悬而未决:(
  • 那么,没有任何解决方案可以在子组件中使用异步验证器吗? angular github repo 上是否有任何官方问题?

标签: angular typescript validation angular-reactive-forms controlvalueaccessor


【解决方案1】:

问题是,当childForm.statusChanges 发出时,异步验证器的订阅已经被取消了。

这是因为childForm.valueChanges 发出之前 childForm.statusChanges

childForm.valueChanges发出时,注册的onChanged回调函数会被调用:

registerOnChange(fn: () => void): void {
  this.childForm.valueChanges.subscribe(fn);
}

这将导致 FormControl(controlChild) 更新其值

function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
  dir.valueAccessor !.registerOnChange((newValue: any) => {
    /* ... */

    if (control.updateOn === 'change') updateControl(control, dir);
  });
}

// Update the MODEL based on the VIEW's value
function updateControl(control: FormControl, dir: NgControl): void {
  /* ... */

  // Will in turn call `control.setValueAndValidity`
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
  /* ... */
}

表示将到达updateValueAndValidity

updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
  this._setInitialStatus();
  this._updateValue();

  if (this.enabled) {
    this._cancelExistingSubscription(); // <- here the existing subscription is cancelled!
    (this as{errors: ValidationErrors | null}).errors = this._runValidator(); // Sync validators
    (this as{status: string}).status = this._calculateStatus(); // VALID | INVALID | PENDING | DISABLED

    if (this.status === VALID || this.status === PENDING) {
      this._runAsyncValidator(opts.emitEvent);
    }
  }

  if (opts.emitEvent !== false) {
    (this.valueChanges as EventEmitter<any>).emit(this.value);
    (this.statusChanges as EventEmitter<string>).emit(this.status);
  }

  if (this._parent && !opts.onlySelf) {
    this._parent.updateValueAndValidity(opts);
  }
}

我提出的方法允许您的自定义组件跳过实现ControlValueAccesorAsyncValidator 接口。

app.component.html

<form [formGroup]="mainForm">
    <child-control></child-control>
</form>

### app.component.ts

  ngOnInit() {
    this.mainForm = this.formBuilder.group({
      childControl: this.formBuilder.control("", null, [this.appValidator.asyncValidation()])
    });
 }

child-control.component.html

<div class="form-group">
  <h4 translate>Child control with async validation: </h4>
  <input type="text" formControlName="childControl">
</div>

child-control.component.ts

@Component({
  templateUrl: "./child-control.component.html",
  selector: "child-control",
  providers: [
  ],
  viewProviders: [
    { provide: ControlContainer, useExisting: FormGroupDirective }
  ]
})
export class ChildControlComponent
  implements OnInit { /* ... */ }

这里的关键是在viewProviders 中提供{ provide: ControlContainer, useExisting: FormGroupDirective }

这是因为在FormControlName 内部,父抽象控件是在@Host 装饰器的帮助下检索的。这允许您在 viewProviders 内部指定要使用 @Host 装饰器声明的依赖项(在本例中为 ControlContainer)。
FormControlName 像这样检索它 @Optional() @Host() @SkipSelf() parent: ControlContainer

StackBlitz.

【讨论】:

    【解决方案2】:

    我尝试了不同的方法和技巧。但是正如 Andrei Gătej 提到的,主窗体取消订阅子控件的更改。

    我的目标是保持自定义控件的独立性,并且不将验证移至主窗体。它花了我一副白发,但我认为我找到了可以接受的解决方法。

    需要在子组件的验证功能内从主窗体传递控制并在那里管理有效性。 在现实生活中,它可能看起来像:

      validate(control: FormControl): Observable<ValidationErrors | null> {
        return this.childForm.statusChanges.pipe(
          map(status => {
            if (status === "VALID") {
              control.setErrors(null);
            }
            if (status === "INVALID") {
              control.setErrors({ invalid: true });
            }
            // you still have to return correct value to mark as PENDING
            return status == "VALID" ? null : { invalid: true };
          }),
        );
      }
    

    【讨论】:

    • 实现了你的方法,但父表单在初始化时处于pending 状态(直到它没有被触及)。之后它可以完美运行。似乎childForm.statusChanges 在第一次从 Pending 更改为 Valid 时没有发出值。此处示例:stackblitz.com/edit/angular-bnub6s.
    【解决方案3】:

    有了这样的验证,一切都很容易。只需要在末尾添加 first()/take(1) 即可关闭 observable 并完成订阅。

     validate(): Observable<ValidationErrors | null> {
        return this.childForm.statusChanges.pipe(
          filter((status) => status !== 'PENDING'),
          map(status => {
            return status == "VALID" ? null : { invalid: true };
          }),
          first(),
        );
      }
    

    但还有另一个问题。

    statusChange 没有在 init 组件上传播状态更改。 Validate 方法已按预期调用,statusChange observable 返回,但异步验证完成后没有任何反应。因此父表单仍处于 PENDING 状态。

    但如果您手动执行此操作(只需更改输入值),则 validate 方法中的 statusChange 会正确触发并返回正确的状态。

    如果能在这方面得到一些帮助会很好。

    这里是更新的代码示例:https://stackblitz.com/edit/angular-cva-async-validation

    【讨论】:

    • 这个问题让我发疯了很长时间。当我将updateValueAndValidity 添加到您的writeValue 方法时,我得到了您的示例,如下所示:writeValue(val: any): void { if (!val) { return; } this.childForm.patchValue(val); setTimeout(() =&gt; { this.childForm.updateValueAndValidity() }, 10) } 但这仍然很hacky。
    • @Daco 是的,有一个技巧。状态没有对初始表单值进行更改,因为模板还没有准备好并且没有订阅适用。超时可以解决这个问题,但我建议使用 ChangeDetectorRef 进行更改检测,然后应用新值。您可以在示例 stackblitz.com/edit/… 第 23 行中看到它是如何工作的
    【解决方案4】:

    表单可能会忽略初始值并卡在 PENDING 直到值更改。

    发生这种情况是因为状态尚未对初始表单值进行更改,因为模板尚未准备好并且没有订阅适用。

    这个问题可以通过 ChangeDetectorRef 解决。 定义表单,检测更改并在此之后应用新值。您可以在示例 https://stackblitz.com/edit/angular-cva-async-validation?file=src%2Fapp%2Fapp.component.ts 第 23 行中看到它是如何工作的

    this.mainForm = this.formBuilder.group({
      mainControl: this.formBuilder.control(''),
    });
    
    // require to set value after view ready
    // otherwise initial state of form will freeze on PENDING
    this.changeDetector.detectChanges();
    this.mainForm.setValue({
        mainControl: {
            childControl: 'test',
        },
    });
    

    【讨论】:

      【解决方案5】:

      为了完整起见,我也在此处添加我的other answer。该方法略有不同,因为它适用于表单的自定义子控件上的 Directive

      我还在那里放置了指向完全正常工作的Stackblitz 的链接。

      【讨论】:

        【解决方案6】:

        我想我会发布另一个解决方案,我在尝试了这里发布的各种其他解决方案后最终使用了它,以防它可能对某人有所帮助。

        在我的例子中,我有多个嵌套的复杂 CVA 组件(4 层深),每个组件都有自己的验证规则,并且在初始化表单时父组件的验证状态没有获取子组件的验证状态(一旦表单是编辑一切都很好)。这是因为父 CVA 组件的验证功能在被检查的 FormGroup/FormArray 的控件由子 CVA 组件验证之前运行。

        要使父组件的有效性状态与其子组件保持同步,请跟踪上一次报告的有效状态是由父组件决定的。然后实现DoCheck接口,如果最后上报的有效状态与当前状态不匹配,则调用onChanges通知父Component。

          ngDoCheck() {
            if (this.lastReportedValidState !== this.form.valid) {
              this.onChanged(this.form.value);
            }
          }
        
          registerOnChange(fn: any): void {
            this.onChanged = fn;
            this.form.valueChanges.subscribe(fn);
          }
        
          validate(control: AbstractControl): ValidationErrors | null {
            this.lastReportedValidState = this.form.valid;
            return this.form.invalid ? { invalid: true } : null;
          }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-01-13
          • 2018-07-17
          • 2021-11-01
          • 2021-01-12
          • 2019-07-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多