Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

I am building an Angular 4 app that requires the BriteVerify email validation on form fields in several components. 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. 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

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>
like image 754
Andre Kuzmicheff Avatar asked Feb 07 '18 03:02

Andre Kuzmicheff


2 Answers

There's a gotcha!

That is, your observable never completes...

This is happening because the observable never completes, so Angular does not know when to change the form status. So remember your observable must to complete.

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.

So you can use first()

UPDATE TO 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, i.e always returns error: STACKBLITZ


OLD:

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

A slightly modified validator, i.e always returns error: STACKBLITZ

like image 100
AT82 Avatar answered Oct 20 '22 23:10

AT82


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

Ex.

In this example I bind (this) to be able to use my service inside the validator function

An extract of my component class 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);       }     );   } });  ... 
like image 26
Miguel Angel Romero Hernandez Avatar answered Oct 20 '22 23:10

Miguel Angel Romero Hernandez