Angular 4:反应式表单控件通过自定义异步验证器陷入挂起状态

问题描述 投票:0回答:6

我正在构建一个 Angular 4 应用程序,它需要对多个组件中的表单字段进行 BriteVerify 电子邮件验证。我正在尝试将此验证实现为自定义异步验证器,我可以将其与反应式表单一起使用。目前,我可以得到 API 响应,但控件状态停留在待处理状态。我没有收到任何错误,所以我有点困惑。请告诉我我做错了什么。这是我的代码。

组件

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
}

验证器类

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;
                    });
            }
        }
    }
}

服务

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
        });
    }
}

模板(只是表格)

<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>

javascript angular angular-reactive-forms angular-validation
6个回答
41
投票

有一个问题

也就是说,你的可观察对象永远不会完成......

发生这种情况是因为可观察对象永远不会完成,因此 Angular 不知道何时更改表单状态。所以请记住您的可观察量必须完成。

您可以通过多种方式完成此操作,例如,您可以调用

first()
方法,或者如果您正在创建自己的可观察对象,则可以在观察者上调用完整方法。

所以你可以使用

first()

更新至 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())
    }
  }
}

稍微修改的验证器,即总是返回错误:STACKBLITZ


旧:

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

稍微修改的验证器,即总是返回错误:STACKBLITZ


2
投票

所以我所做的就是在未获取用户名时抛出 404 并使用订阅错误路径来解析 null,当我确实得到响应时,我解决了一个错误。另一种方法是返回一个数据属性,要么填充用户名的宽度,要么为空 通过响应对象并使用它来代替 404

例如。

在此示例中,我绑定(this)以便能够在验证器函数中使用我的服务

我的组件类 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" });
}

我的验证器类的摘录

//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);
      }
    );
  }
});

...

1
投票

我的做法略有不同,但遇到了同样的问题。

这是我的代码和修复,以防万一有人需要它:

  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;
  }

0
投票

我尝试使用

.first()
。 @AT82描述的技术,但我没有发现它解决了问题。

我最终发现表单状态正在发生变化,但因为我正在使用

onPush
,状态更改没有触发更改检测,因此页面中没有任何更新。

我最终采用的解决方案是:

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())
    }

}

0
投票

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
}


0
投票

就我而言,我在保存之前更新了表单的值和有效性:

public saveValidated() {
    const fullFormGroup = this.clientProfileFormProviderService.getFullForm();
    fullFormGroup.markAllAsTouched();
    updateValueAndValidityRecursively(fullFormGroup);

    if (fullFormGroup.valid) {
      this.saveForm(fullFormGroup);
    } else {
      this.handleInvalidForm(fullFormGroup);
    }
 }

我的异步验证器看起来像这样:

private checkNameUniqueness(currentName: string): AsyncValidatorFn {
    return (control: FormControl) => {
      if (String(control.value) == currentName) {
        return of(null);
      }

      return this.clientProfileApiService.existsClientProfileName(control.value).pipe(
        map(exists => !exists ? null : {nonUnique: true})
      ).pipe(first());
    }
}

那么幕后发生了什么:

  1. 更新值和有效性触发
  2. CheckNameUniqueness 方法被触发
  3. 已向 this.clientProfileApiService.existsClientProfileName 发出请求
  4. fullFormGroup.valid 已检查并返回 false,因为状态为 PENDING
  5. 请求完成
  6. fullFormGroup.status 将其状态更改为 VALID

改变

    return this.clientProfileApiService.existsClientProfileName(control.value).pipe(
        map(exists => !exists ? null : {nonUnique: true})
      ).pipe(first());

进入

    return control.valueChanges.pipe(
    debounceTime(1000),
    distinctUntilChanged(),
    (...)

不会改变任何东西,因为我们每次验证时都会触发该函数。因此,每次我们订阅 valueChanges 时,distinctUntilChanged() 都不起作用,因为它是一个新流!

就我而言,我们有两种可能的解决方案:

  1. 缓存 currentName 并检查函数的值是否已更改,如果没有更改,则返回值而不调用 API。

private checkNameUniqueness(currentName: string): AsyncValidatorFn {
    return (control: FormControl) => {
      if (String(control.value) == currentName) {
        return of(null);
      }
      
      if (CACHED_NAME_VALUE == String(control.value)) {
        return of(null);
      }

      return this.clientProfileApiService.existsClientProfileName(control.value).pipe(
        switchMap(value => this.clientProfileApiService.existsClientProfileName(control.value)),
        map(exists => !exists ? null : {nonUnique: true})
      ).pipe(first());
    }
  }

  1. 将处理表单有效性包装到状态更改流中:

  public saveValidated() {
    const fullFormGroup = this.clientProfileFormProviderService.getFullForm();
    fullFormGroup.markAllAsTouched();
    updateValueAndValidityRecursively(fullFormGroup);

    fullFormGroup.statusChanges.pipe(
      startWith(fullFormGroup.status),
      filter(status => status != 'PENDING'),
      take(1)
    ).subscribe(val => {
      if (fullFormGroup.valid) {
        this.saveForm(fullFormGroup);
      } else {
        this.handleInvalidForm(fullFormGroup);
      }
    });
  }

我选择了选项 2,因为它更通用 - 即使内部没有异步验证器,它也会按预期工作;)

© www.soinside.com 2019 - 2024. All rights reserved.