3

I'm trying to use AsyncValidation in Angular with a service method that has async/await, to test if a userName exists. I can't figure out how to convert the return signature in the service (user.service.ts)

Promise<boolean> 

e.g. 

async isUserNameAvailable(userName: string): Promise<boolean> {
}

to

Promise<ValidationErrors | null> | Observable<ValidationErrors | null>

e.g.

validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
}

in the Validator/Directive.

user.service.ts:

async isUserNameAvailable(userName: string): Promise<boolean> {
    var query = this.db.collection("users").where("name", "==", userName);

    try {
      const documentSnapshot = await query.get();

      if (documentSnapshot.empty) {
        return true;
      } else {
        return false;
      }
    } catch (error) {
      console.log('Error getting documents', error);
    }
  }

existing-username-validator.directive.ts

import { Directive } from '@angular/core';
import { UserService } from './user.service';
import { AbstractControl, ValidationErrors, NG_ASYNC_VALIDATORS, AsyncValidatorFn, AsyncValidator } from '@angular/forms';
import { Observable, timer } from 'rxjs';
import { map, filter, switchMap } from 'rxjs/operators';

export function existingUsernameValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {

    let debounceTime = 500; //milliseconds
    return Observable.timer(debounceTime).switchMap(()=> { //ERROR BECAUSE OF TIMER
      return userService.isUserNameAvailable(control.value).map( //ERROR BECAUSE OF map
        users => {
          return (users && users.length > 0) ? {"usernameExists": true} : null;
        }
      );
    });
  };
} 

@Directive({
  selector: '[appExistingUsernameValidator]',
  providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: ExistingUsernameValidatorDirective, multi: true}]
})
export class ExistingUsernameValidatorDirective implements AsyncValidator {

  constructor(private userService: UserService) {  }

  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return existingUsernameValidator(this.userService)(control);  
  }
}

user.component.ts:

name: new FormControl('', {
      validators: [Validators.required],
      asyncValidators: [existingUsernameValidator(this.userService)]
    }),

Stackblitz: https://stackblitz.com/edit/angular-ivy-cd866c?file=src%2Fapp%2Fuser.service.ts

Does anyone know how to achieve this using Reactive Forms?

2
  • Let me check it up Commented May 10, 2020 at 10:23
  • Thanks a lot. I just added a Stackblitz. Commented May 10, 2020 at 10:29

2 Answers 2

4

Just rectified the existingUsernameValidator in the Stackblitz

export function existingUsernameValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
    let debounceTime = 500; //milliseconds
    const debounceTimer = timer(debounceTime)
    return debounceTimer.pipe(switchMap(()=> {
      return userService.isUserNameAvailable(control.value)
      .then(result => {
          return result ? {"usernameExists": true} : null;
      });
    }));
  };
} 

Updated isUserNameAvailable in UserService

  async isUserNameAvailable(userName: string): Promise<boolean> {
    const query = this.db.collection("users").where("name", "==", userName);

    return query.get()
    .then(function(documentSnapshot) {
      return (documentSnapshot.empty as boolean) 
    })
    .catch(function(error) {
      console.log("Error getting documents: ", error);
      return false;
    });
  }

Try it and let me know if all set now. Also updated on stackblitz

Hope it helps!

EDIT: Code after cleanup

Vaidator Function

export function existingUsernameValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
    const debounceTime = 500; //milliseconds
    return timer(debounceTime).pipe(switchMap(()=> {
      return userService.isUserNameAvailable(control.value)
      .then(result => result ? {"usernameExists": true} : null);
    }));
  };
} 

UserService

async isUserNameAvailable(userName: string): Promise<boolean> {
  return this.db.collection("users").where("name", "==", userName).get()
  .then(documentSnapshot => documentSnapshot.empty as boolean)
  .catch(error => {
    console.log("Error getting documents: ", error);
    return false;
  });
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks again. It's one step closer. Your solution removed the errors coursed by timer() and map(). However, then causes the following error : "Property 'then' does not exist on type 'boolean'".
Ok. Checking on it
1

existingUsernameValidator updated as following,

import {UserService} from './user.service';
import {AbstractControl, ValidationErrors, NG_ASYNC_VALIDATORS, AsyncValidatorFn, AsyncValidator} from '@angular/forms';
import {from, Observable, timer} from 'rxjs';
import {map, debounceTime} from 'rxjs/operators';

export function existingUsernameValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {

    return from(userService.isUserNameAvailable(control.value)).pipe(debounceTime(500),
      map(
        users => {
          return users ? {'usernameExists': true} : null;
        }
      )
    );
  };
}

Hope this will help you

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.