Angular async validator on reactive forms with keystroke throttling and real time password strength meter
In this tutorial we are going to learn how to implement an Angular async validator to validate a password field with a call to a backend service, while also throttling user keystrokes and showing on a progress bar how good the password is.
Some familiarity with npm and Angular is assumed here :)
Setup
Create a new angular project by running
1
2
3
4
5
# if you don't have angular cli, install it first
# npm install -g @angular/cli
ng new async-thrott-val
cd async-thrott-val
ng add @angular/material
For the questions that will pop up during project generation, default should be fine.
Creating the form
Lets add a password input field, a progress bar for showing how good the chosen password is and a button to our app.component.hml
.
Erase everything you find in said file and paste this
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="container">
<form [formGroup]="form">
<mat-form-field class="form-field">
<input matInput formControlName="password" type="password" />
<mat-error *ngIf="form.controls.password.errors">Weak password</mat-error>
</mat-form-field>
<mat-progress-bar
[value]="passScoreBar$ | async"
[color]="form.controls.password.errors && form.controls.password.touched ? 'warn': 'primary'"
></mat-progress-bar>
<button mat-raised-button type="submit" color="primary">Submit</button>
</form>
</div>
Add some styling to app.component.css
just to see things better
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
form {
width: 500px;
}
.form-field {
width: 100%;
}
In app.module.ts
you’ll also need to add the proper material and reactive form modules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
ReactiveFormsModule,
MatProgressBarModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
In AppComponent
, paste the base for our reactive form:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, ChangeDetectionStrategy, OnInit } from "@angular/core";
import { FormGroup, FormBuilder } from "@angular/forms";
import { Observable } from "rxjs";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
form: FormGroup;
passScoreBar$: Observable<number>;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form = this.fb.group({
password: [""]
});
}
}
Everything should be runnable with ng serve
. If it does not work, check all the steps again.
Creating our backend service and validator
Our backend service will be used to validate a score for a given password. To simplify this tutorial, we are not really going to call a real back end. Instead, we are going to implement a mock service that will just evaluate a password based on its length. Our validator will then call this service.
Create a new service by running ng g s password
. The code for it will be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Injectable } from "@angular/core";
import { Observable, of } from "rxjs";
import { map, delay, tap } from "rxjs/operators";
@Injectable({
providedIn: "root"
})
export class PasswordService {
// define the minimum safe score for a password to be used
static MIN_PASSWORD_SCORE = 8;
constructor() {}
// return a password score
getPasswordScore(password: string | undefined): Observable<number> {
password = password || "";
return of(password.length).pipe(
// output to console to show that our 'request' is running
tap(_ => console.log("going to request backend for ", password))
);
}
}
Now let’s create our validator. Create a file inside the app folder called async-pass.validator.ts
and paste this inside:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Injectable } from "@angular/core";
import {
AbstractControl,
AsyncValidator,
ValidationErrors
} from "@angular/forms";
import { Observable, of, Subject, timer } from "rxjs";
import { map, switchMap, tap } from "rxjs/operators";
import { PasswordService } from "./password.service";
@Injectable({ providedIn: "root" })
export class AsyncPassValidator implements AsyncValidator {
constructor(private pwService: PasswordService) {}
validate(
control: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.pwService.getPasswordScore(control.value).pipe(
map(score => {
// if password score is below threshold, validation will fail
if (score < PasswordService.MIN_PASSWORD_SCORE) {
return { unsafe: true };
}
// otherwise, no errors
return null;
})
);
}
}
and update your app.component.ts
by adding the async password validator to the form builder code:
1
2
3
4
5
6
7
8
9
[...]
constructor(private fb: FormBuilder, private pwValidator: AsyncPassValidator) {}
ngOnInit(): void {
this.form = this.fb.group({
password: ['', [], [this.pwValidator.validate.bind(this.pwValidator)]]
});
}
[...]
Now if you run this example with ng s
, you should see that when inputting a password and then ‘submitting’ the form, an error will appear under the input box. If you open the browser inspection tools, you will also see that for every key stroke, a new ‘request’ is being made to the backend (as a console log output), which would spam our server (if we were communicating with one) with requests. Also, the user has no active feedback on how good the password he is typing is. So let’s add some throttling and feedback with that progress bar we already have in our html file.
Adding throttling and password score feedback
Angular async validators will run either immediately for every keystroke, on form submit, or when the input field loses focus, depending on how you configure the validator. We are going to reach a middle point here, were we react to every keypress, but throttle user input so that we don’t spam the server, while still allowing feedback about what was input into the field without having to submit or lose input focus.
Change the validator’s validate
implementation to only start fetching from service after 500ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
validate(
control: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return timer(500).pipe(
switchMap(_ => this.pwService.getPasswordScore(control.value)),
map(score => {
// if password score is below threshold, validation will fail
if (score < PasswordService.MIN_PASSWORD_SCORE) {
return { unsafe: true };
}
// otherwise, no errors
return null;
})
);
}
What will happen here is that with a new control value change, our FormControl will unsubscribe from the previous Observable that was awaiting completion. Therefore, the old Observable will not emit a value, hence no backend call will be made. This logic can be better understood with an image:
Now we just need to show how good the password is while the user types his password. For that, we are going to emit a score value as soon as a call is made to our “backend”, which can then be used as a value in the progress bar. We need to export a Subject
which will emit the score to be read from app.component.ts
.
Let’s change the validate
function again to emit a value every time a request is successfully sent to the backend. For that, add a private variable to AsyncPassValidator
:
1
private scoreEmitter$ = new Subject<number>();
and then also create a getter for this subject:
1
2
3
4
get score(): Observable<number> {
return this.scoreEmitter$;
}
Finally, every time a score is returned from our backend, we will emit it using rxjs
s tap
operator. Right after switchMap(_ => this.pwService.getPasswordScore(control.value)),
introduce a new line with the tap
operator:
1
2
switchMap(_ => this.pwService.getPasswordScore(control.value)),
tap(score => this.scoreEmitter$.next(score)),
Now in our app.component.ts
, inside ngOnInit
and after the initialization of our form code block, we will insert some code to use the value we emitted from the validator as input for our passScoreBar$
. We will need to normalize the password score (which ranges from 0 to 20) to a value that can be used for MatProgressBar
, which ranges from 0 to 100.
1
2
3
4
5
6
7
8
9
10
11
12
this.passScoreBar$ = this.pwValidator.score.pipe(
map(score => {
// first, normalize value for minimum needed password strength.
score =
score > PasswordService.MIN_PASSWORD_SCORE
? PasswordService.MIN_PASSWORD_SCORE
: score;
// then, normalize for progress bar value
return score * (100 / PasswordService.MIN_PASSWORD_SCORE);
})
);
And that should do the trick! If you head over to http://localhost:4200 you should see the progress bar updating after the keystrokes stop for longer than 500ms.
Wrapping up
It can be very frustrating having to enter a lot of data into a form just to find out later on submit that something is missing or wrong. Asynchronous validations are great for giving real time feedback for users when they are typing something into a form that is going to be submitted. Hopefully this can help you implement a similar validation while also sparing your precious server resources to not be fully spammed by the user :) If you want to check the code for this tutorial, head over to the Github repository.