Angular 4,为什么异步验证的嵌套控件不将其有效性传播到父 FormGroup?

In Angular 4, why do asynchronously validated nested controls not propagate their validity to the parent FormGroup?

在我的 Angular 4.0.2 应用程序中,我有一个 FormGroup 包含实现 ControlValueAccessorValidator 的嵌套组件。该组件异步验证自身。问题是,即使嵌套组件有效,父组件 FormGroup 仍然无效。

但是,如果我只是将验证更改为同步,则有效性会正确传播到父组。

我已将其尽可能简化为以下 Plunker:

http://plnkr.co/edit/H26pqEE3sRkzKmmhrBgm

这是显示类似设置的第二个 Plunker,但这次输入 FormControl 直接位于 FormGroup 内部,而不是嵌套组件的一部分。此处,异步验证正确传播:

http://plnkr.co/edit/DSt4ltoO1Cw2Nx1oXxbD

为什么我的异步验证器在组件内部定义时不传播其有效性?


这是第一个(损坏的)Plunker 的代码:

import {Component, NgModule, VERSION, forwardRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import {FormsModule, ReactiveFormsModule} from '@angular/forms'
import {Observable} from 'rxjs/Rx'
import {
    FormGroup, FormControl, FormBuilder, Validators, AbstractControl, Validator,
    ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, ValidationErrors
} from '@angular/forms';


@Component({
  selector: 'my-app',
  template: `
<p>When validated asyncronously, why is the parent form group invalid even though its 
inner control is valid?</p>
<p>Try clearing the value to see the asyncronous validation at work.</p>
<p>Try swapping line 58 for 60 to change to syncronous validation, which propagates correctly.</p>
<hr />
<div><code>FormGroup valid = {{form.valid}}</code></div><br />
<div [formGroup]="form">
  <nested-control formControlName="nested"></nested-control>
</div>
<hr />
`
})
export class App {
  form: FormGroup;
  constructor(fb: FormBuilder) {
    this.form = fb.group({
      nested: ['required']
    });
  }
}


@Component({
    selector: 'nested-control',
    providers: [{
        provide: NG_VALUE_ACCESSOR, 
        useExisting: forwardRef(() => NestedControl),
        multi: true
    }, {
        provide: NG_VALIDATORS,
        useExisting: forwardRef(() => NestedControl),
        multi: true,
    }],
    template: `
<div [formGroup]="form">
  <input type="text" formControlName="input" (keyup)="keyup($event)">
  <code>NestedControl valid = {{form.controls.input.valid}}, errors = {{form.controls.input.errors|json}}</code>
</div>
`
})
export class NestedControl implements ControlValueAccessor, Validator {
  form: FormGroup;
  private propagateChange = () => {};

  constructor(fb: FormBuilder) {
    this.form = fb.group({
      // if we change the validation to be syncronous then everything works as expected:
      //input: [null, Validators.required]
      input: [null, null, this.asyncRequiredValidator]
    });
  }

  asyncRequiredValidator(control: AbstractControl): ValidationErrors {
    // run the built in required validator after 1 second
    return Observable.interval(1000).take(1).map(_ => {
      const result = Validators.required(control);
      console.log('result', result);
      return result;
    });
  }

  keyup() {
    this.propagateChange();
  }

  writeValue(v: any): void {
    this.form.setValue({ input: v });
  }

  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {
  }

  validate(c: AbstractControl): ValidationErrors {
    return this.form.valid ? null : {invalid: true};
  }
}


@NgModule({
  imports: [ BrowserModule, FormsModule, ReactiveFormsModule ],
  declarations: [ App, NestedControl ],
  bootstrap: [ App ]
})
export class AppModule {}

有几个问题我终于弄明白了,但我不确定这个解决方案是否是最优的。

首先,该组件需要提供 NG_ASYNC_VALIDATORS 而不是 NG_VALIDATORS(尽管像往常一样我无法在任何地方找到明确记录的内容):

@Component({
    selector: 'nested-control',
    providers: [{
        provide: NG_ASYNC_VALIDATORS,  // <----- this
        useExisting: forwardRef(() => NestedComponent),
        multi: true,
    } ... 

接下来,它应该实现 AsyncValidator 而不是 Validator:

export class NestedComponent implements ControlValueAccessor, AsyncValidator {
    ...
    validate(c: AbstractControl): Observable<ValidationErrors | null> {
        // must return result wrapped in Observable
        return Observable.of(this.form.valid ? null : {invalid: true});
    }   
}

最后,在验证之后,我们必须明确传播更改。我必须使用 setTimeout 来确保 运行 验证器传播后的状态,而不是当前状态。这对我来说似乎很老套,所以如果有人知道更好的方法,请告诉我。

asyncRequiredValidator(control: AbstractControl): ValidationErrors {
    // run the built in required validator after 1 second
    return Observable.interval(1000).take(1).map(_ => {
      const result = Validators.required(control);
      console.log('result', result);
      setTimeout(() => this.propagateChange());   // <!---- propagate post-validation state
      return result;
    });
  }

这是修复了所有这些问题的 Plunker:

http://plnkr.co/edit/6uSWVhdFgyHTvYjHlgmN?p=preview