Ed Byrne
Ed Byrne

Reputation: 29

Angular2 custom validator with dependency

In Angular 2 I am trying to create my own custom Validator.

I have created my own CustomValidators class, which implements the validator interface.

import { FormControl } from "@angular/forms";
import { MyhttpService } from "./myhttp.service";
import { Response } from "@angular/http";
import { Injectable, Directive } from '@angular/core';


@Injectable()
export class CustomValidators{

constructor(private _http : MyhttpService){

}

public api(c : FormControl)
{
    // Run 
    this._http.get("/expenses/email?email=" + c.value).subscribe((res:Response) => {
        if(res.status === 200){
            // email exists
            return null;
        } else {
            return {"email" : true}
        }
    });
}

If I make api a static method, then I can use the class successfully using.

this._formDetails = fb.group({
  "managerEmail" : ["", [Validators.required, CustomValidators.api] ]
});

However of course this is a static method so I don't have access to any of the constructor values as the constructor has not been run.

Therefore I cannot find a way of implementing custom validator with dependencies, there must be a way.

I have tried listing CustomValidators as a provider so my class receives an instantiated object of the class.

Property 'api' does not exist on type 'typeof CustomValidators'

Am I providing the class in the correct way? Why does the method not exist?

Upvotes: 2

Views: 1635

Answers (2)

Cole Cooper
Cole Cooper

Reputation: 11

I ran into a similar situation using Angular v5.2.9 with verifying that a username does not already exist in my database. My use case is a little different—I'm using a small user list which is easily cached, and my data is centralized using the @ngrx library, but I hope it can be helpful.

Starting with the Validator class, the constructor is responsible for making the fetch request and caching the results in a static list observable; this list observable will be used by the actual validation method to see if the username is already in use.

import { Injectable } from '@angular/core'
import { FormControl } from '@angular/forms'

import { Store } from '@ngrx/store'

import { Observable } from 'rxjs/observable'
import 'rxjs/add/operator/take'
import 'rxjs/add/operator/map'

import { myActions } from '../@ngrx/actions/some.actions'
import { State, selectIds } from '../@ngrx/reducers/some.reducer'


@Injectable()
export class CustomValidators {

  static ids_in_use$: Observable<string[]>;

  constructor(
    private store: Store<State>
  ) {
    this.store.dispatch({ type: myActions.FETCH_REQUEST })
    CustomValidators.ids_in_use$ = this.store
      .select( selectIds )
      .map( ( id_list: string[] ) => id_list.map( id => id.toLowerCase() ) )
  }

  static api( control: FormControl ) {
    return new Promise(
      ( resolve ) => {
        CustomValidators.ids_in_use$
          .take( 1 )
          .subscribe(
            id_list => {
              if( id_list.indexOf( control.value.toLowerCase() ) === -1 ) 
                resolve( null )
              else resolve({ 'email-in-use': true })
            })
      })
}

In order to circumvent the lack of access to instance properties in static methods, the validator's constructor is responsible for setting the static properties. Because this class is decorated with @Injectable(), it can be dependency-injected to the constructor of the component that's using it:

constructor(
  ...,
  private fb: FormBuilder,
  private customValidators: CustomValidators
) { }

This is how I'm able to ensure that the code in the validator's constructor is executed despite the main validation logic being a static method. Similarly, I would imagine that you could use this instance to make use of any instance properties/methods you specifically need prior to the validation—in your case, making the http request. I can then use the static validation method in the FormBuilder group (bear in mind, unless you make a call to it, your tslint will warn you that 'customValidators' is declared but its value is never read)

this._formDetails = fb.group({
  'managerEmail': [ '', Validators.required, CustomValidators.api ]
})

Finally, a provider must be declared for the injectable service, which can be done in the @Component decorator:

@Component({
  ...,
  providers: [ CustomValidators ]
})

Upvotes: 1

Mike Airey
Mike Airey

Reputation: 143

Try this:

this._formDetails = fb.group({
  "managerEmail" : ["", Validators.required, CustomValidators.api]
});

Validators.required is a synchronous validator, but your CustomValidators.api is an asynchronous validator.

Per the official documentation, each form control should be specified with state, synchronous validator(s), then async validator(s).

Upvotes: 0

Related Questions