6

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.

Community
  • 1
  • 1
JSON
  • 1,583
  • 4
  • 31
  • 63
  • 1
    Do you need to validate the email based on another control? If so the best practice is to lift the validator to the parent control. I can provide the example if need be. – Tomasz Kula Apr 04 '18 at 07:20
  • @TomaszKula, the email control belongs to a `formGroup`, do I need to group the email control with another one? I would appreciate the example – JSON Apr 04 '18 at 07:24
  • 2
    Sure, give me 10 mins, and I'll create something for you :) – Tomasz Kula Apr 04 '18 at 07:25
  • @TomaszKula, Thanks :) – JSON Apr 04 '18 at 07:28

2 Answers2

1

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

Tomasz Kula
  • 16,199
  • 2
  • 68
  • 79
  • will `personalEmailValidator ` run `async` in this case? – JSON Apr 04 '18 at 07:41
  • 1
    in my example I'm using a simple validation function. If you need to do some http requests it would be better to implement the validator as a regular class and implement the `AsyncValidator` interface. That way you can inject http into the constructor do whatever async stuff you need in the `validate` method. – Tomasz Kula Apr 04 '18 at 07:47
  • can you please show me a brief example on how to add the async functionality to the group validator? – JSON Apr 05 '18 at 05:57
0

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,

JSON
  • 1,583
  • 4
  • 31
  • 63