简体   繁体   English

Angular 4:使用自定义异步验证器,反应式表单控件卡在挂起的 state

[英]Angular 4: reactive form control is stuck in pending state with a custom async validator

I am building an Angular 4 app that requires the BriteVerify email validation on form fields in several components.我正在构建一个 Angular 4 应用程序,它需要对多个组件中的表单字段进行 BriteVerify email 验证。 I am trying to implement this validation as a custom async validator that I can use with reactive forms. Currently, I can get the API response, but the control status is stuck in pending state. I get no errors so I am a bit confused.我正在尝试将此验证实现为自定义异步验证器,我可以将其与反应式 forms 一起使用。目前,我可以获得 API 响应,但控制状态停留在待定 state。我没有收到任何错误,所以我有点困惑。 Please tell me what I am doing wrong.请告诉我我做错了什么。 Here is my code.这是我的代码。

Component成分

 import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { EmailValidationService } from '../services/email-validation.service'; import { CustomValidators } from '../utilities/custom-validators/custom-validators'; @Component({ templateUrl: './email-form.component.html', styleUrls: ['./email-form.component.sass'] }) export class EmailFormComponent implements OnInit { public emailForm: FormGroup; public formSubmitted: Boolean; public emailSent: Boolean; constructor( private router: Router, private builder: FormBuilder, private service: EmailValidationService ) { } ngOnInit() { this.formSubmitted = false; this.emailForm = this.builder.group({ email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ] }); } get email() { return this.emailForm.get('email'); } // rest of logic }

Validator class验证人 class

 import { AbstractControl } from '@angular/forms'; import { EmailValidationService } from '../../services/email-validation.service'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; export class CustomValidators { static briteVerifyValidator(service: EmailValidationService) { return (control: AbstractControl) => { if (.control.valueChanges) { return Observable;of(null). } else { return control.valueChanges.debounceTime(1000).distinctUntilChanged().switchMap(value => service.validateEmail(value)).map(data => { return data?status === 'invalid': { invalid: true }; null; }); } } } }

Service服务

 import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; interface EmailValidationResponse { address: string, account: string, domain: string, status: string, connected: string, disposable: boolean, role_address: boolean, error_code?: string, error?: string, duration: number } @Injectable() export class EmailValidationService { public emailValidationUrl = 'https://briteverifyendpoint.com'; constructor( private http: HttpClient ) { } validateEmail(value) { let params = new HttpParams(); params = params.append('address', value); return this.http.get<EmailValidationResponse>(this.emailValidationUrl, { params: params }); } }

Template (just form)模板(只是形式)

 <form class="email-form" [formGroup]="emailForm" (ngSubmit)="sendEmail()"> <div class="row"> <div class="col-md-12 col-sm-12 col-xs-12"> <fieldset class="form-group required" [ngClass]="{ 'has-error': email.invalid && formSubmitted }"> <div>{{ email.status }}</div> <label class="control-label" for="email">Email</label> <input class="form-control input-lg" name="email" id="email" formControlName="email"> <ng-container *ngIf="email.invalid && formSubmitted"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>&nbsp;Please enter valid email address. </ng-container> </fieldset> <button type="submit" class="btn btn-primary btn-lg btn-block">Send</button> </div> </div> </form>

There's a gotcha !有一个问题

That is, your observable never completes...也就是说,你的 observable 永远不会完成......

This is happening because the observable never completes, so Angular does not know when to change the form status.这是因为 observable 永远不会完成,所以 Angular 不知道何时更改表单状态。 So remember your observable must to complete.所以记住你的 observable 必须完成。

You can accomplish this in many ways, for example, you can call the first() method, or if you are creating your own observable, you can call the complete method on the observer.您可以通过多种方式完成此操作,例如,您可以调用first()方法,或者如果您正在创建自己的 observable,您可以在观察者上调用 complete 方法。

So you can use first()所以你可以使用first()

UPDATE TO RXJS 6: RXJS 6 更新:

briteVerifyValidator(service: Service) {
  return (control: AbstractControl) => {
    if (!control.valueChanges) {
      return of(null);
    } else {
      return control.valueChanges.pipe(
        debounceTime(1000),
        distinctUntilChanged(),
        switchMap(value => service.getData(value)),
        map(data => {
          return data.status === 'invalid' ? { invalid: true } : null;
        })
      ).pipe(first())
    }
  }
}

A slightly modified validator, ie always returns error: STACKBLITZ一个稍微修改的验证器,即总是返回错误: STACKBLITZ


OLD:旧:

.map(data => {
   return data.status === 'invalid' ? { invalid: true } : null;
})
.first();

A slightly modified validator, ie always returns error: STACKBLITZ一个稍微修改的验证器,即总是返回错误: STACKBLITZ

So what I did was to throw a 404 when the username was not taken and use the subscribe error path to resolve for null, and when I did get a response I resolved with an error.所以我所做的是在没有使用用户名时抛出 404 并使用订阅错误路径来解析为空,当我得到响应时,我解决了一个错误。 Another way would be to return a data property either filled width the username or empty through the response object and use that insead of the 404另一种方法是返回一个数据属性,要么填充用户名的宽度,要么通过响应对象为空,并使用 404 的 insead

Ex.例如。

In this example I bind (this) to be able to use my service inside the validator function在这个例子中,我绑定 (this) 以便能够在验证器函数中使用我的服务

An extract of my component class ngOnInit()我的组件类 ngOnInit() 的摘录

//signup.component.ts

constructor(
 private authService: AuthServic //this will be included with bind(this)
) {

ngOnInit() {

 this.user = new FormGroup(
   {
    email: new FormControl("", Validators.required),
    username: new FormControl(
      "",
      Validators.required,
      CustomUserValidators.usernameUniqueValidator.bind(this) //the whole class
    ),
    password: new FormControl("", Validators.required),
   },
   { updateOn: "blur" });
}

An extract from my validator class来自我的验证器类的摘录

//user.validator.ts
...

static async usernameUniqueValidator(
   control: FormControl
): Promise<ValidationErrors | null> {

 let controlBind = this as any;
 let authService = controlBind.authService as AuthService;  
 //I just added types to be able to get my functions as I type 

 return new Promise(resolve => {
  if (control.value == "") {
    resolve(null);
  } else {
    authService.checkUsername(control.value).subscribe(
      () => {
        resolve({
          usernameExists: {
            valid: false
          }
        });
      },
      () => {
        resolve(null);
      }
    );
  }
});

...

I've been doing it slightly differently and faced the same issue.我的做法略有不同,并面临着同样的问题。

Here is my code and the fix in case if someone would need it:这是我的代码和修复,以防万一有人需要它:

  forbiddenNames(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise<any>((resolve, reject) => {
      setTimeout(() => {
        if (control.value.toUpperCase() === 'TEST') {
          resolve({'nameIsForbidden': true});
        } else {

          return null;//HERE YOU SHOULD RETURN resolve(null) instead of just null
        }
      }, 1);
    });
    return promise;
  }

I tries using the .first() .我尝试使用.first() technique described by @AT82 but I didn't find it solved the problem. @AT82 描述的技术,但我没有发现它解决了问题。

What I eventually discovered was that the form status was changing but it because I'm using onPush , the status change wasn't triggering change detection so nothing was updating in the page.我最终发现表单状态正在发生变化,但因为我使用的是onPush ,状态变化没有触发变化检测,所以页面中没有任何更新。

The solution I ended up going with was:我最终采用的解决方案是:

export class EmailFormComponent implements OnInit {
    ...
    constructor(
        ...
        private changeDetector: ChangeDetectorRef,
    ) {

      ...

      // Subscribe to status changes on the form
      // and use the statusChange to trigger changeDetection
      this.myForm.statusChanges.pipe(
        distinctUntilChanged()
      ).subscribe(() => this.changeDetector.markForCheck())
    }

}

 import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { EmailValidationService } from '../services/email-validation.service'; import { CustomValidators } from '../utilities/custom-validators/custom-validators'; @Component({ templateUrl: './email-form.component.html', styleUrls: ['./email-form.component.sass'] }) export class EmailFormComponent implements OnInit { public emailForm: FormGroup; public formSubmitted: Boolean; public emailSent: Boolean; constructor( private router: Router, private builder: FormBuilder, private service: EmailValidationService ) { } ngOnInit() { this.formSubmitted = false; this.emailForm = this.builder.group({ email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ] }); } get email() { return this.emailForm.get('email'); } // rest of logic }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM