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-ngxs-plugin/sign-up-form-ngxs-plugin.component.html -->
<h1>Sign Up Form (NGXS with form-plugin)</h1>
<mat-card>
<form
ngxsForm="signUpFormNgxsPlugin.signUpForm"
[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$ | async) || !signUpForm.valid"
>
Submit
</button>
</div>
</form>
</mat-card>
<h2>User (JSON)</h2>
<pre
>{{ user$ | async | json }}
</pre>
<h2>Form state (JSON)</h2>
<pre
>{{ signUpFormState$ | async | json }}
</pre>
// app/async-validation/sign-up-form-ngxs-plugin/sign-up-form-ngxs-plugin.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { UniqueUsernameValidator } from '../unique-username-validator';
import { User } from '../user';
import { SignUpFormNgxsPlugin } from './sign-up-form-ngxs-plugin.actions';
import { SignUpForm } from './sign-up-form-ngxs-plugin.state';
import { SignUpFormNgxsPluginSelectors } from './sign-up-form-ngxs-plugin.selectors';
@Component({
selector: 'app-sign-up-form-ngxs-plugin',
templateUrl: './sign-up-form-ngxs-plugin.component.html',
})
export class SignUpFormNgxsPluginComponent {
readonly signUpForm = this.formBuilder.group({
username: ['', Validators.required, [this.uniqueUsernameValidator]],
password: ['', Validators.required],
});
loading = false;
@Select(SignUpFormNgxsPluginSelectors.form)
signUpFormState$!: Observable<SignUpForm>;
@Select(SignUpFormNgxsPluginSelectors.model)
user$!: Observable<User>;
@Select(SignUpFormNgxsPluginSelectors.loading)
loading$!: Observable<boolean>;
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 store: Store,
private readonly snackBar: MatSnackBar
) {}
submit(): void {
this.store
.dispatch(new SignUpFormNgxsPlugin.Submit())
.pipe(withLatestFrom(this.user$))
.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));
}
}
// app/async-validation/sign-up-form-ngxs-plugin/sign-up-form-ngxs-plugin.state.ts
import { Injectable } from '@angular/core';
import { Action, State, StateContext } from '@ngxs/store';
import { finalize, tap } from 'rxjs/operators';
import { User } from '../user';
import { UserService } from '../user.service';
import { SignUpFormNgxsPlugin } from './sign-up-form-ngxs-plugin.actions';
export interface SignUpForm {
model: User;
status: string;
dirty: boolean;
}
export interface SignUpFormNgxsPluginStateModel {
signUpForm: SignUpForm;
loading: boolean;
}
@State<SignUpFormNgxsPluginStateModel>({
name: 'signUpFormNgxsPlugin',
defaults: {
signUpForm: {
model: {
id: 0,
username: '',
password: '',
},
status: 'INVALID',
dirty: false,
},
loading: false,
},
})
@Injectable()
export class SignUpFormNgxsPluginState {
constructor(private readonly userService: UserService) {}
@Action(SignUpFormNgxsPlugin.Submit)
submit(context: StateContext<SignUpFormNgxsPluginStateModel>) {
const state = context.getState();
context.patchState({
loading: true,
});
return this.userService.save(state.signUpForm.model).pipe(
tap(({ id }) =>
context.patchState({
signUpForm: {
...state.signUpForm,
model: {
...state.signUpForm.model,
id,
},
},
})
),
finalize(() =>
context.patchState({
loading: false,
})
)
);
}
}
// app/async-validation/sign-up-form-ngxs-plugin/sign-up-form-ngxs-plugin.actions.ts
export namespace SignUpFormNgxsPlugin {
export class Submit {
static readonly type = '[SignUpFormNgxsPlugin] Submit';
}
}
// app/async-validation/sign-up-form-ngxs-plugin/sign-up-form-ngxs-plugin.selectors.ts
import { Selector } from '@ngxs/store';
import { User } from '../user';
import {
SignUpForm,
SignUpFormNgxsPluginState,
SignUpFormNgxsPluginStateModel,
} from './sign-up-form-ngxs-plugin.state';
export class SignUpFormNgxsPluginSelectors {
@Selector([SignUpFormNgxsPluginState])
static form({ signUpForm }: SignUpFormNgxsPluginStateModel): SignUpForm {
return signUpForm;
}
@Selector([SignUpFormNgxsPluginSelectors.form])
static model({ model }: SignUpForm): User {
return model;
}
@Selector([SignUpFormNgxsPluginState])
static loading({ loading }: SignUpFormNgxsPluginStateModel): boolean {
return loading;
}
}