2
votes

See updates below

I realize my problem is that I'm very green at observables and RxJS.

I have a custom validator like this:

export function ValidateFinalQty(service: MyService) {
    return (control: AbstractControl): { [key: string]: any } | null => {
        let qty = service.GetQty();
        if (control.value != qty) {
            return { FinalQuantityNotMatching: true };
        } else {
            return null;
        }
    };
}

GetQty returns an RxJS Observable. So how can I set this up so that my synchronous validator returns the right value depending on an asynchronous call? I need the return type of the validator to stay as { [key: string]: any } | null.

I saw a suggestion of something like qty = await service.GetQty().first().toPromise(); but then I am returning a promise and I can't return a promise for the validator to work as I understand.

How can I handle this?

From my package.json:

"@angular/core": "7.1.0",
"@angular/forms": "7.1.0",
"rxjs": "6.3.3",
"rxjs-compat": "^6.4.0",

UPDATE 5/23/19 Trying to Implement @Sachin's Answer. My breakpoints inside of map never get hit. I don't get any console logs and even if I remove the logic in the map and return null, it still always returns invalid. Very confused about what's happening here. My service is actually getting called, I've confirmed that.

Any thoughts?

export class CustomAsyncValidator {
    static ValidateFinalQty(qtyService: FinalQtyService, brf: BatchRecordForm): AsyncValidatorFn {
        return (control: AbstractControl) => {
            return qtyService.postCalcFinalQuanity(brf)
                .pipe(
                    map((qty) => {
                        console.log("running qty validator. value:", qty);
                        if (control.value !== qty) {
                            return { FinalQuantityNotMatching: true };
                        } else {
                            return null;
                        }
                    }),
                    catchError((err) => {
                        console.log("Error in final quantity validator", err);
                        return null;
                    }),
                    finalize(() => console.log("finished"))
                );
        };
    }
}

UPDATE 6/7/2019

At the subscribe logging I am getting the right answer (null or { FinalQuantityNotMatching: true }) but my form control remains invalid. What am I doing wrong?

Validator.ts

export class CustomAsyncValidator {
    static ValidateFinalQty(fqs: FinalQtyService, brf: BatchRecordForm) {
        return (control: AbstractControl) => {
            return fqs.postCalcFinalQuanity(brf).pipe(
                debounceTime(500),
                tap((action) => console.log("final qty", action)),
                tap((action) => console.log("control.value", control.value)),
                map(arr => (arr.Value !== `${control.value}`) ? { FinalQuantityNotMatching: true } : null)
            ).subscribe(x => console.log("subscribe output", x));
        };
    }
}

component.ts

 this.noteForm.addControl(this.finalQtyFormControlName, new FormControl(this.noteSubModuleForm.Value,
        [Validators.required, CustomAsyncValidator.ValidateFinalQty(this.finalQtyService, this.embrService.batchRecordForm)]));

UPDATE 6/7/2019 #2

Following https://www.youtube.com/watch?v=zeX5CtFqkXQ I was able to make a directive based validator but would still prefer the have the validator in my ts if you're able to see what I did wrong in the previous update.

@Directive({ selector: "[validFinalQty]", providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ValidateFinalQtyDirective, multi: true }] })

export class ValidateFinalQtyDirective implements AsyncValidator {

    constructor(private fqs: FinalQtyService, private embrService: EmbrService) { }

    validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
        return this.fqs.postCalcFinalQuanity(this.embrService.batchRecordForm).pipe(
            tap(x => {
                console.log("final qty", x);
                console.log("control.value", control.value);
            }),
            map(arr => (arr.Value !== `${control.value}`) ? { FinalQuantityNotMatching: true } : null)
        );
    }
3
Can you show the implementation of getQty() ? - asimhashmi
.subscribe(x => console.log("subscribe output", x)); this subscription is wrong. You should pass the observable without subscribing - Avin Kavish

3 Answers

1
votes

I have a similar kind of Validator directive, let me adjust according to your code:

See if it can work for you

import { Directive } from '@angular/core';
import { NG_ASYNC_VALIDATORS, AsyncValidator, AbstractControl, ValidationErrors } from '@angular/forms';
import { MyService } from './MyService';
import { Observable,  of as observableOf} from 'rxjs';


@Directive({
  selector: '[qty-valid]',
  providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: QuantityValidatorDirective , multi: true}]
})
export class QuantityValidatorDirective implements AsyncValidator {    
   constructor(private service : MyService ) { }

    validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {

        return new Promise((resolve, reject) => {                
                this.service.GetQty()
                    .subscribe( (qty: any) => {
                       if (control.value != qty) {
                          resolve({ FinalQuantityNotMatching: true })
                       } else {
                          resolve(null)
                       }

                },error => resolve(null));
        });
    }
}
1
votes

Hope this helps

export class CustomAsyncValidator {
  static ValidateFinalQty(apiService: ApiService):AsyncValidatorFn {
    return (control: AbstractControl) => {
      const value = control.value;
      return apiService.GetQty()
        .pipe(
          take(1),
          map(qty => {
            if (value!=qty) {
              return { FinalQuantityNotMatching: true };
            } else {
              return null;
            }
          })
        );
    };
  }
}

How to use this

this.yourForm = this.fb.group({
  yourField: ["",CustomValidator.ValidateFinalQty(this.apiService)]
});
1
votes

As your Validator send async RxJS call, it will be a good idea to use AsyncValidator

(Also see the working demo I created for you)

From the docs:

constructor(formState: any = null, validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[])

As you can see that you can pass AsyncValidatorFn as a third argument to your FormControl.

Assuming that getQty() returns Observable that emits the value that you need to compare. Your custom Validator will be like this:

custom.validator.ts

import { AbstractControl } from '@angular/forms';
import { MyService } from './my.service';
import { map } from 'rxjs/operators';
export class ValidateFinalQty {
  static createValidator(myService: MyService) {
    return (control: AbstractControl) => {
      return myService.getQty().pipe(map(res => {
        return control.value == res ? null :  { FinalQuantityNotMatching: true };
      }));
    };
  }
}

Here I've created a static method with will accept your service and perform the call and give error if condition doesn't match.

One thing I noticed in your code is that instead of subscribing to Observable returned by getQty(), you are assigning it the value to qty variable which is not a correct way to deal with Observable. You can learn more about Observables here

Now in your component:

import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators, FormBuilder } from '@angular/forms'
import { ValidateFinalQty } from './custom.validator';
import { MyService } from './my.service';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  implements OnInit{
  name = 'Angular';
  myForm: FormGroup

  constructor(private myService: MyService, private fb: FormBuilder) {

  }
  ngOnInit() {
    this.myForm = this.fb.group({
    quantity: [
      '',
      [Validators.required],
      ValidateFinalQty.createValidator(this.myService)
    ]
  });
}
}

And in your HTML:

<form [formGroup]="myForm">
  <label>Quantity</label>
  <input type="number" formControlName="quantity" placeholder="Enter quantity to validate">

  <div *ngIf="myForm.get('quantity').status === 'PENDING'">
    Checking...
  </div>
  <div *ngIf="myForm.get('quantity').status === 'VALID'">
    😺 Quantity is valid!
  </div>

  <div *ngIf="myForm.get('quantity').errors && myForm.get('quantity').errors.FinalQuantityNotMatching">
    😢 Oh noes, your quantity is not valid!
  </div>
</form>


<small>5 is valid quantity for the sake of example</small>

Hope it helps.