I had the same problem. I wanted a solution for debouncing the input and only request the backend when the input changed.
All workarounds with a timer in the validator have the problem, that they request the backend with every keystroke. They only debounce the validation response. That's not what's intended to do. You want the input to be debounced and distincted and only after that to request the backend.
My solution for that is the following (using reactive forms and material2):
The component
@Component({
selector: 'prefix-username',
templateUrl: './username.component.html',
styleUrls: ['./username.component.css']
})
export class UsernameComponent implements OnInit, OnDestroy {
usernameControl: FormControl;
destroyed$ = new Subject<void>(); // observes if component is destroyed
validated$: Subject<boolean>; // observes if validation responses
changed$: Subject<string>; // observes changes on username
constructor(
private fb: FormBuilder,
private service: UsernameService,
) {
this.createForm();
}
ngOnInit() {
this.changed$ = new Subject<string>();
this.changed$
// only take until component destroyed
.takeUntil(this.destroyed$)
// at this point the input gets debounced
.debounceTime(300)
// only request the backend if changed
.distinctUntilChanged()
.subscribe(username => {
this.service.isUsernameReserved(username)
.subscribe(reserved => this.validated$.next(reserved));
});
this.validated$ = new Subject<boolean>();
this.validated$.takeUntil(this.destroyed$); // only take until component not destroyed
}
ngOnDestroy(): void {
this.destroyed$.next(); // complete all listening observers
}
createForm(): void {
this.usernameControl = this.fb.control(
'',
[
Validators.required,
],
[
this.usernameValodator()
]);
}
usernameValodator(): AsyncValidatorFn {
return (c: AbstractControl) => {
const obs = this.validated$
// get a new observable
.asObservable()
// only take until component destroyed
.takeUntil(this.destroyed$)
// only take one item
.take(1)
// map the error
.map(reserved => reserved ? {reserved: true} : null);
// fire the changed value of control
this.changed$.next(c.value);
return obs;
}
}
}
The template
<mat-form-field>
<input
type="text"
placeholder="Username"
matInput
formControlName="username"
required/>
<mat-hint align="end">Your username</mat-hint>
</mat-form-field>
<ng-template ngProjectAs="mat-error" bind-ngIf="usernameControl.invalid && (usernameControl.dirty || usernameControl.touched) && usernameControl.errors.reserved">
<mat-error>Sorry, you can't use this username</mat-error>
</ng-template>