AsyncValidator
, the validate()
method is called on each keystroke, and doesn't
allow you to debounce.
markAsPending()
. When the validation ends call
setErrors()
.
ngModel
.ngModel.pending
// app/async-validation/user.ts
export interface User {
id: number;
username: string;
password: string;
}
// app/async-validation/unique-username-validator.directive.ts
import { Directive, OnDestroy } from '@angular/core';
import { NgModel } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime, filter, switchMap, tap } from 'rxjs/operators';
import { UserService } from './user.service';
// For more context see: https://mobiarch.wordpress.com/2017/12/26/angular-async-validator/
@Directive({
selector: '[appUniqueUsername]',
})
export class UniqueUsernameValidatorDirective implements OnDestroy {
private readonly subscription?: Subscription;
constructor(userService: UserService, { control }: NgModel) {
this.subscription = control.valueChanges
.pipe(
// If you filter out empty strings, you cannot use distinctUntilChanged() later
filter((username) => !!username),
tap(() => control.markAsPending()),
debounceTime(500),
switchMap((username) => userService.isUsernameAvailable(username))
)
.subscribe((available) => {
if (!control.value || available) {
// This is needed to report the validation is not pending anymore
return control.setErrors(control.errors);
}
control.setErrors({
...control.errors,
uniqueUsername: { value: control.value },
});
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
<!-- app/async-validation/sign-up-form-template/sign-up-form-template.component.html -->
<h1>Sign Up Form (template-based)</h1>
<mat-card>
<form (ngSubmit)="submit()" #signUpForm="ngForm">
<mat-form-field>
<mat-label>Username</mat-label>
<input
matInput
required
appUniqueUsername
name="username"
[(ngModel)]="user.username"
#username="ngModel"
/>
<mat-spinner
matSuffix
[diameter]="18"
*ngIf="username.pending"
></mat-spinner>
<mat-error *ngIf="username.errors?.required"
>You must enter a value</mat-error
>
<mat-error *ngIf="username.errors?.uniqueUsername"
>The username has already been taken</mat-error
>
</mat-form-field>
<mat-form-field>
<mat-label>Password</mat-label>
<input
matInput
required
type="password"
name="password"
[(ngModel)]="user.password"
#password="ngModel"
/>
<mat-error *ngIf="password.errors?.required"
>You must enter a value</mat-error
>
</mat-form-field>
<div class="form-buttons">
<button
mat-raised-button
type="submit"
color="primary"
[disabled]="loading || !signUpForm.form.valid"
>
Submit
</button>
</div>
</form>
</mat-card>
<h2>User (JSON)</h2>
<pre
>{{ user | json }}
</pre>
// app/async-validation/sign-up-form-template/sign-up-form-template.component.ts
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { finalize } from 'rxjs/operators';
import { User } from '../user';
import { UserService } from '../user.service';
@Component({
selector: 'app-sign-up-form-template',
templateUrl: './sign-up-form-template.component.html',
})
export class SignUpFormTemplateComponent {
readonly user: User = {
id: 0,
username: '',
password: '',
};
loading = false;
constructor(
private readonly userService: UserService,
private readonly snackBar: MatSnackBar
) {}
submit(): void {
this.loading = true;
this.userService
.save(this.user)
.pipe(finalize(() => (this.loading = false)))
.subscribe(({ id }) => {
this.snackBar.open(`User saved with ID #${id}`, 'Close');
});
}
}
// app/async-validation/user.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { User } from './user';
export interface UserSaveResponse {
id: number;
}
@Injectable({
providedIn: 'root',
})
export class UserService {
isUsernameAvailable(username: string): Observable<boolean> {
console.log('Is username available?', username);
// Let's say that all usernames with a dot are taken
return of(username.indexOf('.') === -1 ? true : false).pipe(delay(1000));
}
save(user: User): Observable<UserSaveResponse> {
console.log('Save:', user);
return of({
id: 1,
}).pipe(delay(1000));
}
}