2

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)
        );
    }
2
  • Can you show the implementation of getQty() ? Commented Jun 12, 2019 at 13:13
  • .subscribe(x => console.log("subscribe output", x)); this subscription is wrong. You should pass the observable without subscribing Commented Jun 12, 2019 at 14:01

3 Answers 3

1

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));
        });
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

How would you use a validator like this with the directive? I tried something like new FormControl(null, [Validators.required, QuantityValidatorDirective.validate(myService)]
So how would I change this be able to call it like new FormControl(null, [Validators.required, QuantityValidatorDirective.validate(myService)] ?
I don't know what your are trying with new FormControl... You can use this directive as any other directive, just put the qty-valid as element attribute
1

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>

Comments

1
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)]
});

5 Comments

I tried your answer but haven't been able to get it to work, the code inside of map just doesn't seem to call. I've updated my question above.
@azulBonnet , I have updated my code with take(1) operator in rxjs .
it just chokes on the map somehow, it doesn't throw any errors and never gets to either return.
Okay, I think I've just realized that it was because it was a "cold" observable and needed to add the subscribe at the end and that's why nothing was getting called.
@azulBonnet observable can be finite or infinite . Just to ensure if thats the case did u tried using take(1) ?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.