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;
}
}
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 != '');
}
}