Reactive Validation Triggers in Angular Forms

Reactive Validation Triggers in Angular Forms

Have you ever wanted to create a form that tracks user behavior and automatically manages validations? With the use of Angular Reactive forms, you can do just that. This blog will teach you how to:

Create a form from scratch
keep a field clean before it is interacted with
display validations on touch
clear validations while the user is typing
if invalid, trigger validation as soon as the user stops typing
trigger validations in case the user clicks on the submit button before interacting with mandatory fields

Setup

Creating a new Angular project

ng new my-app –standalone=false

Create a new module and a new component.

ng g m todos/todo-form
ng g c todos/todo-form

Creating Modules

Todo Form Module

Add the Reactive Forms Module to your module file:

@NgModule({
declarations: [
TodoFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule
],
exports: [
TodoFormComponent
]
})
export class TodoFormModule { }

Add Todo Form Module to the App Module

app.module.ts

import { NgModule } from @angular/core;
import { BrowserModule } from @angular/platform-browser;

import { AppComponent } from ./app.component;
import { TodoFormModule } from ./todos/todo-form/todo-form.module;

@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
TodoFormModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Now you should be able to use a todo form in App Component.

app.component.ts

<app-todo-form></app-todo-form>

Creating form skeleton

The form contains two fields title and description.

todo-form.component.ts

import { AbstractControl, FormBuilder, FormGroup, Validators } from @angular/forms;

enum FormFields {
Title = title,
Description = description
}

@Component({
selector: app-todo-form,
templateUrl: ./todo-form.component.html,
styleUrls: [./todo-form.component.scss],
})
export class TodoFormComponent implements OnInit {
todoForm!: FormGroup;

constructor(
private fb: FormBuilder
) {}

ngOnInit(): void {
this.setupForm();
}

// form structure
private setupForm(): void {
this.todoForm = this.fb.group({
[FormFields.Title]: [], // default values
[FormFields.Description]: []
});
}

onSubmit(): void {
}

}

todo-form.component.html

<form [formGroup]=“todoForm” (ngSubmit)=“onSubmit()”>

<div class=“title-wrapper”>
<input formControlName=“title” type=“text” />
</div>

<div class=“description-wrapper”>
<textarea formControlName=“description”></textarea>
</div>

<!– This button invokes onSubmit() method upon clicking –>
<button class=“submit-btn” type=“submit”>Submit</button>

</form>

Validations

Adding Form Validations

The todo title has several validations:

it’s required
has a minimum length of characters
has a maximum length of 100 characters
must use only letters (Regex)

The description field has only length validation – up to 300 characters allowed.

private setupForm(): void {
this.todoForm = this.fb.group({
[FormFields.Title]: [
,
[
Validators.required,
Validators.minLength(3),
Validators.maxLength(100),
Validators.pattern(new RegExp(/^[a-zA-Zs]+$/)),
],
],
[FormFields.Description]: [, [Validators.maxLength(300)]],
});
}

Create Getters

Create a getter for each form control to access it in the template easily:

get titleControl(): AbstractControl {
return this.todoForm.get(FormFields.Title) as AbstractControl;
}

get descriptionControl(): AbstractControl {
return this.todoForm.get(FormFields.Description) as AbstractControl;
}

Applying Validations in the template

todo-form.component.html

<!– Title –>

<div class=“title-wrapper”>
<label>Title</label>
<input formControlName=“title” type=“text” />
<div class=“error-message” *ngIf=“titleControl.invalid && (titleControl.dirty || titleControl.touched)”>
<div *ngIf=“titleControl.hasError(‘required’)”>Title is required</div>
<div *ngIf=“titleControl.hasError(‘maxlength’)”>Title is too long</div>
<div *ngIf=“titleControl.hasError(‘minlength’)”>Title is too short</div>
<div *ngIf=“titleControl.hasError(‘pattern’)”>Title contains numbers or symbols</div>
</div>
</div>

<!– Description –>

<div class=“description-wrapper”>
<label>Description</label>
<textarea formControlName=“description”></textarea>
<div class=“error-message” *ngIf=“descriptionControl.invalid && (descriptionControl.dirty || descriptionControl.touched)”>
Maximum number of characters is 300!
</div>
</div>

Clarifications

To prevent errors from popping up as soon as the page loads, you want to add the following line as a prerequisite:

titleControl.invalid && (titleControl.dirty || titleControl.touched)

What it does is validate field behavior.

If control validation failed, control.invalid will be true

If the control is dirty or touched means the user either clicked or started typing on input

Once the user interacts with the form and the form is in an invalid state, then you proceed with validating each validation rule set in the TypeScript component file:

Component
Template

Validators.required
titleControl.hasError(‘required’)

Validators.minLength(3)
titleControl.hasError(‘minlength’)

Validators.maxLength(100)
titleControl.hasError(‘maxlength’)

Validators.pattern(new RegExp(…))
titleControl.hasError(‘pattern’)

And display an appropriate error for each.

Trigger validations after the user stops typing

Create a dictionary-like structure that holds form fields.

todo-form.component.ts

formFieldCanBeValidated = {
[FormFields.Title]: true,
[FormFields.Description]: true
}

The FormGroup (TodoForm) exposes a valueChanges property that is available on each form control. The valueChanges returns an Observable, that you can pipe operators to and then subscribe.

In this case, use the debounceTime(n) operator that emits the Observable only after the n number of milliseconds passed.
In between, toggle field state via this.formFieldCanBeValidated[field] using the tap() operator and the subscribe() function.

Now create a function that toggles validation rules when a user starts and stops typing.

todo-form.component.ts

// this will enable/disable validation for each field (title or description)
private toggleValidationRules(field: FormFields) {
this.todoForm.get(field)?.valueChanges
.pipe(
// clear validation as soon the user starts typing
tap(() => this.formFieldCanBeValidated[field] = false),
// hold for 500ms after user stopped typing
debounceTime(500),
)
// set validation when user stops
.subscribe(() => this.formFieldCanBeValidated[field] = true)
}

Call the function above in ngOnInit for each form field

ngOnInit(): void {
this.setupForm();

this.toggleValidationRules(FormFields.Title)
this.toggleValidationRules(FormFields.Description)
}

Finally, use formFieldCanBeValidated in the template.

todo-form.component.html

<div class=“title-wrapper”>
<label>Title</label>
<input formControlName=“title” type=“text” />
<div class=“error-message” *ngIf=“formFieldCanBeValidated[‘title’] && titleControl.invalid && (titleControl.dirty || titleControl.touched)”>
<div *ngIf=“titleControl.hasError(‘required’)”>Title is required</div>
<div *ngIf=“titleControl.hasError(‘maxlength’)”>Title is too long</div>
<div *ngIf=“titleControl.hasError(‘minlength’)”>Title is too short</div>
<div *ngIf=“titleControl.hasError(‘pattern’)”>Title contains numbers or symbols</div>
</div>
</div>

This ensures that validation and error messages in the template are displayed only when a user stops using the form field for 500ms or in other words, when formFieldCanBeValidated[‘title’] = true.

Prevent form submit

Using Reactive forms you can prevent form submission if the form is in an invalid state. You can verify the form validity using the valid property on the FormGroup (TodoForm).

todo-form.component.ts

onSubmit(): void {
// will not pass this line if there is any error on the form
if (!this.todoForm.valid) {
return
}

// read form values
console.log(this.todoForm.values);

// {title: ‘Hello’, description: ‘World’}

}

A common practice is to disable the submit button until the form is in a valid state.

<!– This button invokes onSubmit() method upon clicking –>
<button class=“submit-btn” type=“submit” [disabled]=“!todoForm.valid”>Submit</button>

However, you can trigger validation rules to display on UI on submit if the user hasn’t interacted with the form.

Create a function that validates all form fields and marks them as touched:

todo-form.component.ts

private triggerValidationOnSubmit() {
Object.keys(this.todoForm.controls).forEach(field => {
const control = this.todoForm.get(field);
control.markAsTouched({ onlySelf: true });
});
}

Apply the previous function in the onSubmit() call.

onSubmit(): void {
// will not pass this line if there is any error on the form
if (!this.todoForm.valid) {
this.triggerValidationOnSubmit();
return;
}

// … do other stuff

Now, when a user hits the submit button ahead of time, it will display errors for all invalid fields.

Clean Observables

Finally, unsubscribe from all active Observables to prevent memory leaks.

1) Create a subject to track Observables

todo-form.component.ts

private readonly unsubscribed$ = new Subject<void>();

2) Put the subject inside the pipe

private toggleValidationRules(field: FormFields) {
this.todoForm.get(field)?.valueChanges
.pipe(
tap(() => this.formFieldCanBeValidated[field] = false),
debounceTime(500),
takeUntil(this.unsubscribed$) // <– subject to unsubscribe
)
.subscribe(() => this.formFieldCanBeValidated[field] = true)
}

3) Unsubscribe once the component is destroyed

export class TodoFormComponent implements OnInit, OnDestroy


ngOnDestroy(): void {
this.unsubscribed$.next();
this.unsubscribed$.complete();
}

That’s all from me today.
If you’d like to learn more be sure to check out my other blog on Medium and follow me on Twitter to stay up to date with my content updates.

Get Full Code

Bye for now 👋

Leave a Reply

Your email address will not be published. Required fields are marked *