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.