AsyncValidator
.delay
to avoid doing too many HTTP requests.
FormBuilder
.ngModel.pending
// app/async-validation/user.ts
export interface User {
id: number;
username: string;
password: string;
}
// app/async-validation/unique-username-validator.ts
import { Injectable } from '@angular/core';
import {
AbstractControl,
AsyncValidator,
ValidationErrors,
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { delay, filter, map, switchMap } from 'rxjs/operators';
import { UserService } from './user.service';
// For more context see: https://stackoverflow.com/a/62662296
@Injectable({
providedIn: 'root',
})
export class UniqueUsernameValidator implements AsyncValidator {
constructor(private readonly userService: UserService) {}
validate(
control: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return of(control.value).pipe(
filter((username) => !!username),
delay(500),
switchMap((username) =>
this.userService
.isUsernameAvailable(username)
.pipe(
map((available) =>
available ? null : { uniqueUsername: { value: control.value } }
)
)
)
);
}
}
<!-- app/async-validation/sign-up-form-reactive/sign-up-form-reactive.component.html -->
<h1>Sign Up Form (reactive)</h1>
<mat-card>
<form [formGroup]="signUpForm" (ngSubmit)="submit()">
<mat-form-field>
<mat-label>Username</mat-label>
<input matInput name="username" formControlName="username" />
<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
type="password"
name="password"
formControlName="password"
/>
<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.valid"
>
Submit
</button>
</div>
</form>
</mat-card>
<h2>User (JSON)</h2>
<pre
>{{ signUpForm.value | json }}
</pre>
// app/async-validation/sign-up-form-reactive/sign-up-form-reactive.component.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { finalize } from 'rxjs/operators';
import { UniqueUsernameValidator } from '../unique-username-validator';
import { User } from '../user';
import { UserService } from '../user.service';
@Component({
selector: 'app-sign-up-form-reactive',
templateUrl: './sign-up-form-reactive.component.html',
})
export class SignUpFormReactiveComponent {
readonly signUpForm = this.formBuilder.group({
username: ['', Validators.required, [this.uniqueUsernameValidator]],
password: ['', Validators.required],
});
loading = false;
get username() {
return this.signUpForm.get('username');
}
get password() {
return this.signUpForm.get('password');
}
constructor(
private readonly formBuilder: FormBuilder,
private readonly uniqueUsernameValidator: UniqueUsernameValidator,
private readonly userService: UserService,
private readonly snackBar: MatSnackBar
) {}
submit(): void {
const user: User = this.signUpForm.value;
this.loading = true;
this.userService
.save(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));
}
}