RRGT19
RRGT19

Reputation: 1687

Async validator is not setting the returned error in the FormControl

I'm doing a web application in Angular 8 and Bootstrap. I have a form with just one input to enter a 5 digits number (as string). I want to validate that this number is unique in the database.

I have tried to implement an async validator. It checks if the number is unique, if it's not, should return an error to display a message in the template.

brand: Brand;
gambleForm: FormGroup;

constructor(
    private brandService: BrandService,
    private fb: FormBuilder,
  ) {
    this.gambleForm = fb.group({
      number: ['', [ // Default validators
        Validators.required,
        Validators.minLength(5)
      ]]
    });
  }

ngOnInit() {
    // Get the id of the entity coming from the router
    const id = this.route.snapshot.paramMap.get('id');
    this.getBrandData(id);
  }

getBrandData(id: string) {
    this.brandService.getBrandById(id).subscribe(res => {
      this.brand = res;
      this.setNumberAsyncValidator();
    });
  }

// Set async validator
setNumberAsyncValidator() {
    this.gambleForm.get('number').setAsyncValidators([
      CustomValidator.checkNumberDisponibility(
        this.brandService,
        this.brand.id,
        'jsjdlnsdf3jn234'
      )
    ]);
  }

CustomValidators class:

import {AbstractControl, AsyncValidatorFn} from '@angular/forms';
import {catchError, debounceTime, map, switchMap} from 'rxjs/operators';
import {Observable, of} from 'rxjs';
import {BrandService} from '../core/services/brand.service';

function isEmptyInputValue(value: any): boolean {
  return value === null || value.length === 0;
}

export class CustomValidator {

  static checkNumberDisponibility(brandService: BrandService, brandId: string, raffleId: string): AsyncValidatorFn {
    return (control: AbstractControl):
      Promise<{ [key: string]: any } | null>
      | Observable<{ [key: string]: any } | null> => {
      if (isEmptyInputValue(control.value)) {
        return of(null);
      } else if (control.value.length !== 5) {
        return of(null);
      } else {
        return control.valueChanges.pipe(
          debounceTime(500),
          switchMap(_ =>
            // This method returns: Observable<any[]>
            brandService.getGamblesWithTheNumber(brandId, raffleId, control.value)
              .pipe(
                map(gambles => {
                  // The "exhausted" error is not present in the FormControl
                  return gambles.length ? {exhausted: true} : null;
                }),
                catchError(err => {
                  console.log('Number validator error: ' + err);
                  return of(null);
                })
              )
          )
        );
      }
    };
  }

}

The error that I'm returning is not present in the FormControl, therefore, my custom message is not shown in the template. In other words, the FormControl has nothing inside of its "errors" object.

I have tried with no result:

setNumberAsyncValidator() {
    this.gambleForm.get('number').setAsyncValidators([
      CustomValidator.checkNumberDisponibility(
        ...
      )
    ]);
    this.gambleForm.get('number').updateValueAndValidity(); // Notice this
  }

However, doing this does work:

map(gambles => {
  return gambles.length ? control.setErrors({exhausted: true}) : null;
}),

Also, doing this does work too:

return control.valueChanges.pipe(
          debounceTime(500),
          switchMap(_ =>
            brandService.getGamblesWithTheNumber(brandId, raffleId, control.value)
              .pipe(
                map(gambles => {
                  console.log('Gambles count: ' + gambles.length);
                  return gambles.length ? {exhausted: true} : null;
                }),
                catchError(err => {
                  console.log('Number validator error: ' + err);
                  return of(null);
                })
              )
          ),
          first(), // Notice this
        );

Perhaps the Observable returned was not completed correctly and that is why I needed to use first ()? I'm confused here.

All the examples of async validators that I have found, uses the first approach, returning {exhausted: true}. For some reason it's not working for me. My second approach, using control.setErrors() (I was testing and doing trial and error and it worked), I don't think that is the best way to do it. Even Angular's documentation doesn't do it this way.

Why the return of {exhausted: true} is not present in the FormControl's errors? What I am missing?

My goal is to return {exhausted: true} correctly and have it inside of the FormControl errors to show my custom message in the template.

Upvotes: 1

Views: 5823

Answers (2)

Jesse
Jesse

Reputation: 2607

It looks like you're doing everything right, my only guess is you need to call updateValueAndValidity() after setting the async validator.

From the documentation on the method setAsyncValidators:

Sets the async validators that are active on this control. Calling this overwrites any existing async validators.

When you add or remove a validator at run time, you must call updateValueAndValidity() for the new validation to take effect.

Hopefully that does the trick.

UPDATED

I'm just now seeing the point you called out about first(). That operator will kill your subscription after the first value. I'm unsure exactly how your service call is being made, but if you're relying on more than the first input from the control, then that could be the issue.

Upvotes: 1

Tidiane Rolland
Tidiane Rolland

Reputation: 51

You are using the FormBuilder, why don't you register the Async Validator in the declaration of your input in the Form?

Like this

constructor(
    private brandService: BrandService,
    private fb: FormBuilder,
  ) {
    this.gambleForm = fb.group({
      number: ['', 
        [ // Default validators
          Validators.required,
          Validators.minLength(5)
        ],
        [ //Async Validator
         MyAsyncValidator
        ]
      ]
    });
  }

Where MyAsyncValidator is a Async Validator Function

Upvotes: 3

Related Questions