1

I build a standard register form with Angular and I want to validate if the email is already taken with an async directive (UserExistsDirective), then manage the error message with a second directive (ValidityStyleDirective). The second directive is used on other inputs as well.

ValidityStyleDirective needs to wait until UserExistsDirective completed before execute. Otherwise, the error message will be out of sync.

Here the code I've done so far, I struggle to find the right solution so I need help! Again, UserExistsDirective needs to complete before goto ValidityStyleDirective. How can I do that with Angular ?

Thank you,

HTML :

...
<div class="mb-4 inputcontainer">
                    <label class="form-label" for="email">Courriel</label>
                    <input appUserExists validityStyle formControlName="email" class="form-control" id="email" type="email" name="email" >
                    <div *ngIf="registerForm.get('email')?.pending" class="icon-container">
                        <i class="loader"></i>
                    </div>
                    <div class="valid-feedback">Looks good!</div>
                    <div class="invalid-feedback">
                        <span *ngIf="registerForm.get('email')?.hasError('required')">Please provide an email.</span>
                        <span *ngIf="registerForm.get('email')?.errors?.['pattern']">Please provide a valid email.</span>
                        <span *ngIf="registerForm.get('email')?.errors?.['userExists']">This email address is already registered. Please use another one.</span>
                    </div>
                </div>
...

UserExistsDirective : it add a the key userExists to validator if an email is found.

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

  }
  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.emailExists(control.value).pipe(
      take(1),
      map(isTaken => (isTaken ? { userExists: true } : null)),
      catchError(() => of(null))
    );
  }

  emailExists(email: string): Observable<boolean> {
    return of(email).pipe(
      delay(500),
      map((email) => {
        const emails = ['123@outlook.com', '123@gmail.com'];
        return emails.includes(email);
      })
    );
  }
}

Then ValidityStyleDirective add the error CSS class if needed

@Directive({
    selector: '[validityStyle]'
})
export class ValidityStyleDirective {

    constructor(@Self() private ngControl: NgControl,
        private formGroup: FormGroupDirective,
        private renderer: Renderer2,
        private el: ElementRef) { }
    
    @HostListener('blur')
    onBlur($event: any) {
        if (this.ngControl.errors == null) {
            this.formGroup.control.get('email')?.setErrors(null);
        }

        if (this.ngControl.value === '') {
            this.renderer.removeClass(this.el.nativeElement, 'is-invalid');
            return false;
        }

        if (!this.ngControl.valid) {
            this.renderer.addClass(this.el.nativeElement, 'is-invalid');
            return true;
        }

        this.renderer.removeClass(this.el.nativeElement, 'is-invalid');
        return false;
    }
}

  • According to https://stackoverflow.com/a/38847773/10315665 the order of execution is undefined. So you can't depend on it. You could technically combine those two directives somehow, e.g. have a two step thing where you call into `validateSyle` only after `appUsersExists` has finished. Other than that, I personally don't think it really matters in what order the error shows up. **Just a suggestion:** I wouldn't even show the email error in the form, but like larger and highlighted above or below the form (e.g. next to the submit button). – Elias Jul 07 '22 at 22:13
  • Checking if the email is already taken does also not necessarily need to be reactive. Just do it when the submit button is pressed, before the actual create user event (or whatever) is sent to the server. – Elias Jul 07 '22 at 22:14

1 Answers1

1

I found my answer. I share it in case it could help someone else.

I got the result I wanted by removing/adding the IS-INVALID class from appUserExists directive

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

  constructor(private renderer: Renderer2,
    private el: ElementRef) {

  }

  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.emailExists(control.value).pipe(
      first(),
      map(isTaken => (isTaken ? { userExists: true } : null)),
      catchError(() => of(null))
    );
  }

  emailExists(email: string): Observable<boolean> {
    return of(email).pipe(
      delay(500),
      map((email) => {
        const emails = ['123@outlook.com', '123@gmail.com'];
        const isTaken = emails.includes(email);

        isTaken
          ? this.renderer.addClass(this.el.nativeElement, 'is-invalid')
          : this.renderer.removeClass(this.el.nativeElement, 'is-invalid');

        return isTaken;
      })
    );
  }
}

Then I got no more out of sync error message by using STATUS instead of VALID property

@Directive({
    selector: '[validityStyle]'
})
export class ValidityStyleDirective {
    constructor(@Self() private ngControl: NgControl
    , private formGroup: FormGroupDirective) { }
  
    @HostBinding('class.is-invalid')
    get setInvalid() {
        return  (this.formGroup as FormGroupDirective)?.submitted && this.ngControl.status == 'INVALID' 
        || (this.ngControl.dirty && this.ngControl.status == 'INVALID' && this.ngControl.value != '');
    }
}
  • 1
    great, congratulations. Thanks for sharing. Accept your answer as the solution too – Obum Jul 08 '22 at 13:09