JSON
JSON

Reputation: 1623

How to pass params to async validator for reactive angular control?

Problem,

I am using the same component for read/edit routines. async-validator works perfectly with new entries. the problem starts if the user accidentally changes the value and tries to revert back to the saved value. my current code will run regardless and returns the value as existing. I want to pass more data along with control value so I can validate if that pair value exist already or not.

I am posting the relevant code,

this is my form control,

patientEmail: new FormControl(
    null,
    [Validators.email, Validators.required],
    FormControlValidator.createEmailAsyncValidator(
      this.asyncValidatorService
    ),
  ),

my async validator creator class is,

export class FormControlValidator {
  static createEmailAsyncValidator(asyncValidatorService: AsyncValidationService) {
    return (control: AbstractControl) => {
      if (!control.pristine) {
        control.markAsPristine();
        return asyncValidatorService
          .validateEmailNotTaken(control)
          .map((response: HttpResponse<boolean>) => {
            return !response.body ? null : { taken: true };
          });
      }
      return Observable.of(null);
    };
  }

and finally my service,

@Injectable()
export class AsyncValidationService {
  constructor(private httpService: HttpClientService) {}

  public validateEmailNotTaken(control: AbstractControl) {
    return this.httpService.getRequest(
      'PatientsRegistration/IsPatientEmailExist?email=' + control.value,
    );
  }
}

I want to be able to pass another param to my createEmailAsyncValidator, something like another control value from the form if possible.

Upvotes: 6

Views: 8224

Answers (2)

Tomasz Kula
Tomasz Kula

Reputation: 16837

If you need to validate control based on another control, you need to lift the validator to the parent control. Here's an example of validator that checks if the email starts with person's name (another control value).

app.component.ts

import { Component } from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ValidatorFn} from '@angular/forms'

const personalEmailValidator: ValidatorFn = (ctrl: FormGroup) => {
  const email = ctrl.get('email') as FormControl;
  const name = ctrl.get('name') as FormControl;

  const valid = (email.value || '').startsWith(name.value);

  return valid ? null : { personalEmailError: 'Email must start with a person name'}
}

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      email: [null],
      name: [null]
    }, { validator: personalEmailValidator })
  }
}

app.component.html

<input [formControl]="form.get('name')" placeholder="name" />
<input [formControl]="form.get('email')" placeholder="email" />

{{ form.hasError('personalEmailError') ? form.getError('personalEmailError') : 'form is valid'}}

Live demo

Upvotes: 1

JSON
JSON

Reputation: 1623

So I worked it in another way, inspired by Tomasz Kula answer

I created an Async Directive that implements AsyncValidator interface, I pass extra params as an `object, in my case it looks like this,

{
 coupledControl: AbstractControl,
 method: {
          apiUrl: string
         }
} 

this is my directive code,

import { Directive, forwardRef, Input } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { HttpClientService } from '../../../../../shared/services/httpclient.service';
import { HttpResponse } from '@angular/common/http';
import { IAsyncMethod } from './../interfaces/async-methods-interface';


@Directive({
  // tslint:disable-next-line:directive-selector
  selector: '[asyncValidator][formControlName], [asyncValidator][ngModel]',
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: forwardRef(() => AsynValidatorDirective),
      multi: true,
    },
  ],
})
export class AsynValidatorDirective implements Validator {
  @Input() asyncValidator: { coupledControl: AbstractControl; method: IAsyncMethod };

  validate(
    control: AbstractControl,
  ): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> {
    return this.validateIsExistAsync(control);
  }

  constructor(private httpService: HttpClientService) {}
  validateIsExistAsync(control: AbstractControl) {
    if (!control.pristine) {
      const coupledControl: AbstractControl = this.asyncValidator.coupledControl;
      const method: IAsyncMethod = this.asyncValidator.method;
      return this.httpService
        .getRequest(method.apiUrl + '?control=' + control.value + '&id=' + coupledControl.value)
        .map((response: HttpResponse<boolean>) => {
          return !response.body ? null : { asyncInvalid: true };
        });
    }
    return Observable.of(null);
  }
}

in my HTML,

 <input [asyncValidator]="{coupledControl: patientRestrationForm.get('patientFileId'), method: this.asyncMethods.validatePatientEmail }" [errorStateMatcher]="matcher" matInput autocomplete="off" formControlName="patientEmail">

and in my backend I check for both exist and match, simple logic!

Would appreciate any input,

Upvotes: 0

Related Questions