I have User Form where two inputs 'Username' and 'Email' must to be checked for duplication. ▼
this.userForm = this.formBuilder.group({
username: [
this.isEdit ? this.data.user?.nameIdentifier : null,
Validators.required
],
email: [
this.isEdit ? this.data.user?.email : null,
[Validators.required, Validators.email]
]
});
I have custom validator which needs async data to be loaded, and then uses that data to check for duplication. So I placed that validator in subscribe, and I am setting that validation to the form control when async data loads. ▼
this.userService
.getUsers({ pageSize: 5000 })
.pipe(takeUntil(this.destroy$))
.subscribe((users: IGetUsersRes) => {
this.userForm
.get('username')
?.setValidators(
duplicateNameValidator(
users.list, // this one
['nameIdentifier'],
this.data.user
)
);
this.userForm
.get('email')
?.setValidators(
duplicateNameValidator(
users.list, // this one
['email'],
this.data.user
)
);
});
This worked back then, but now when I am switching to my Custom Input this validation from Subscribe doesn't get assigned to Custom Input (maybe because it is in async). My custom input takes parent Form Control validations in this way ▼
export class InputComponent implements OnInit, OnDestroy, ControlValueAccessor {
inputControl = new FormControl();
isDisabled!: boolean;
@Input() debounce = false;
onChange: any = () => {
// any
};
onTouched: any = () => {
// any
};
constructor(@Self() @Optional() public ngControl: NgControl) {
this.ngControl && (this.ngControl.valueAccessor = this);
}
ngOnInit(): void {
this.updateInputValue();
this.setValidators();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
setValidators(): void {
const control = this.ngControl.control;
const validators: ValidatorFn[] = control?.validator
? [control?.validator]
: [];
this.inputControl.setValidators(validators);
}
updateInputValue(): void {
this.inputControl.valueChanges
.pipe(
takeUntil(this.destroy$),
debounceTime(this.debounce ? 500 : 0),
distinctUntilChanged()
)
.subscribe((res) => {
this.onChange(res);
});
}
clearInput(): void {
this.inputControl.reset(null, { emitEvent: false });
this.onChange();
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
writeValue(value: any): void {
this.inputControl.setValue(value);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(disabled: boolean): void {
this.isDisabled = disabled;
}
}
I am not sure that I am doing everything correct, so does anybody know how to fix this issue? I think Parent Control validations are assigned on ngOnInit but since my duplicateNameValidator() is in async it needs additional treatment, and I have no idea how to see it in my Custom Input. Searching in Google has poor results, google gets misleaded.
CodePudding user response:
If you want to do something async with reactive forms, we have async validators just for that! So in the first place I would have used that, so I suggest the following...
Create a validation function. I here pass the form controlname and an observable of the http request result as parameters. For fetching the observable we assign it in the component, attach a shareReplay
so that the request is only made once.
Here I lazily use http directly from my component, use a service like you are already doing ;)
users$ = this.http.get('https://jsonplaceholder.typicode.com/users').pipe(
shareReplay(1)
);
We then pass user$
to the async validator. We then use this validator in a similar fashion as custom sync validators, but the async validator needs to be the third argument:
this.reactiveForm = this.fb.group({
username: ['', [], [validate('username', this.users$)]],
email: ['', [], [validate('email', this.users$)]],
});
The validation function:
export function validate(ctrlName: string, result$): ValidatorFn {
return (ctrl: AbstractControl): ValidationErrors | null => {
if (ctrl) {
return result$.pipe(
map((values: any) => {
const found = values.find((x) => x[ctrlName].toLowerCase() === ctrl.value.toLowerCase());
if (found) {
return { notValid: true };
}
return null;
}),
catchError(() => of(null))
);
}
return of(null);
};
}
So this same validator will work both for your username and email, as we pass the control name as string.
Then you can in your custom component display this error just like any other sync validator:
<input type="text" [formControl]="$any(control)">
<small *ngIf="control.hasError('notValid')">Value already taken!</small>
Here is a STACKBLITZ with the above code - using a custom input as well.