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.
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> 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...
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
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); } ); } }); ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With