Reputation: 1687
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
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.
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
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