1

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.

3
  • u need to return errors in setNumberAsyncValidator() functuion. And add this function to validate of number form control in gambleForm. Commented Jan 15, 2020 at 4:45
  • or see this alligator.io/angular/async-validators Commented Jan 15, 2020 at 4:45
  • You have to use cold observable in your async validators. Commented Mar 2, 2021 at 12:38

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

1 Comment

I'm assign it later because I need to pass a few variables to the async validator, which are not available inside of the construction of the form.
1

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.

1 Comment

I have tried that but it doesn't work. I have used this.gambleForm.get('number').updateValueAndValidity(); right after setting the async validator with no effect at all. The error from the validator is still not present. Any other idea?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.