17

I have a problem with defining asynchrous validator in template driven form.

Currently i have this input:

<input type="text" ngControl="email"  [(ngModel)]="model.applicant.contact.email" #email="ngForm" required asyncEmailValidator>

with validator selector asyncEmailValidator which is pointing to this class:

import {provide} from "angular2/core";
import {Directive} from "angular2/core";
import {NG_VALIDATORS} from "angular2/common";
import {Validator} from "angular2/common";
import {Control} from "angular2/common";
import {AccountService} from "../services/account.service";

@Directive({
selector: '[asyncEmailValidator]',
providers: [provide(NG_VALIDATORS, {useExisting: EmailValidator, multi: true}), AccountService]
})

export class EmailValidator implements Validator {
//https://angular.io/docs/ts/latest/api/common/Validator-interface.html


constructor(private accountService:AccountService) {
}

validate(c:Control):{[key: string]: any} {
    let EMAIL_REGEXP = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i;

    if (!EMAIL_REGEXP.test(c.value)) {
        return {validateEmail: {valid: false}};
    }

    return null;

    /*return new Promise(resolve =>
        this.accountService.getUserNames(c.value).subscribe(res => {
            if (res == true) {
                resolve(null);
            }
            else {
                resolve({validateEmailTaken: {valid: false}});
            }
        }));*/
}

}

Email regex part is working as expected and form is being validated successfuly if regex is matching. But after that I want to check if e-mail is not already in use, so im creating promise for my accountService. But this doesn't work at all and form is in failed state all the time.

I've read about model driven forms and using FormBuilder as below:

constructor(builder: FormBuilder) {
this.email = new Control('',
  Validators.compose([Validators.required, CustomValidators.emailFormat]), CustomValidators.duplicated
);
}

Which have async validators defined in third parameter of Control() But this is not my case because im using diffrent approach.

So, my question is: is it possible to create async validator using template driven forms?

Marduk
  • 359
  • 4
  • 13

3 Answers3

20

You could try to register the provider of your async validator with the NG_ASYNC_VALIDATORS key and not the NG_VALIDATORS one (only for synchronous validators):

@Directive({
  selector: '[asyncEmailValidator]',
  providers: [
    provide(NG_ASYNC_VALIDATORS, { // <------------
      useExisting: EmailValidator, multi: true
    }),
    AccountService
  ]
})
export class EmailValidator implements Validator {
  constructor(private accountService:AccountService) {
  }

  validate(c:Control) {
    return new Promise(resolve =>
      this.accountService.getUserNames(c.value).subscribe(res => {
        if (res == true) {
            resolve(null);
        }
        else {
            resolve({validateEmailTaken: {valid: false}});
        }
    }));
  }
}

See this doc on the angular.io website:

J J B
  • 8,540
  • 1
  • 28
  • 42
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Thank you! This is exactly what I've wanted. Btw. Is it possible to combine async part of validator (promise) with noasync part (just regex) in one validator class in such a way that async part will be started only if regex is OK or do I need both of them separately? – Marduk Mar 26 '16 at 17:16
  • Yes you can but you need in both cases to resolve the promise. The latter can be resolved even if processing isn't asynchronous... – Thierry Templier Mar 27 '16 at 06:28
  • Ok, I'll try it. Thanks once more. – Marduk Mar 28 '16 at 21:30
  • 1
    Thank you for building on the existing documentation at https://angular.io/guide/form-validation this helps so much! – Manuel Hernandez Mar 26 '18 at 19:38
3

worth noting that the syntax has changed since then, now i am using angular 4, and here below a rewrite:

import { Directive, forwardRef } from '@angular/core';
import { AbstractControl, Validator, NG_ASYNC_VALIDATORS } from '@angular/forms';
import { AccountService } from 'account.service';

@Directive({
    selector: '[asyncEmailValidator]',
    providers: [
        {
            provide: NG_ASYNC_VALIDATORS,
            useExisting: forwardRef(() => EmailValidatorDirective), multi: true
        },
    ]
})
export class EmailValidatorDirective implements Validator {
    constructor(private _accountService: AccountService) {
    }

    validate(c: AbstractControl) {
        return new Promise(resolve =>
            this._accountService.isEmailExists(c.value).subscribe(res => {
                if (res == true) {
                    resolve({ validateEmailTaken: { valid: false } });
                }
                else {
                    resolve(null);
                }
            }));
    }
}
jemilg
  • 319
  • 2
  • 4
0

I am able to correctly call validate custom validators using user service. One problem i was getting was that, I kept my custom validator inside Validators.compose(). After taking out of the compose function everything works.

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

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

        const q = new Promise((resolve, reject) => {
            setTimeout(() => {
                userService.isUsernameTaken(control.value).subscribe((data: any) => {
                    // console.log('top: ' + data + ' type: ' + typeof data);
                    if (data === false) {
                        resolve(null);
                    } else {
                        resolve({
                            usernameTaken: {
                                valid: true
                            }
                        });
                    }
                }, () => {
                    resolve({
                        usernameTaken: {
                            valid: false
                        }
                    });
                });
            }, 1000);
        });

        return q;
    };
}

@Directive({
    selector: '[appUniqueUsername]',
    providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: UniqueUsernameValidatorDirective, multi: true }, UserService]
})
export class UniqueUsernameValidatorDirective implements AsyncValidator {
    constructor(private userService: UserService) { }

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

}
prisar
  • 3,041
  • 2
  • 26
  • 27