Reactive Validation Triggers in Angular Forms
Tags: angular, rxjs, forms
Published: 2024-03-30 19:11:08

---
title: Reactive Validation Triggers in Angular Forms
published: true
description: 
tags: angular, rxjs, forms 
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryo12aogrnu6avnj1zhw.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-03-30 06:04 +0000
---

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

<img width="100%" style="width:100%" src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExd3VuN2lxcnQxZzd0ZG9oOTBtZXY5d3FnaHRjZ3J6emlyYTFkMW9rYSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/44cKJGjASL7tkNi8oB/giphy.gif">

## Setup

### Creating a new Angular project

```bash
ng new my-app --standalone=false
```

Create a new module and a new component.

```bash
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:

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

#### Add Todo Form Module to the App Module

##### app.module.ts

```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

```html
  <app-todo-form></app-todo-form>
```

## Creating form skeleton

The form contains two fields title and description.

##### todo-form.component.ts

```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

```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.

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

### Create Getters

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


```ts
  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

```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:

```ts
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

```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

```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

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

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

Finally, use `formFieldCanBeValidated` in the template.

##### todo-form.component.html

```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

```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.

```html
<!--  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

```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.

```ts

  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.

<img width="100%" style="width:100%" src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZXJieHN6Y2lhdjdkNGRkcjBvcXN5NWJra2lpOGY4dW5xZG9kendtcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XxxzRTdBRdvsFzjNhf/giphy.gif">


## Clean Observables

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

#### 1) Create a subject to track Observables

##### todo-form.component.ts

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

#### 2) Put the subject inside the pipe

```ts
  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

```ts
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](https://mirzaleka.medium.com) and follow me on [Twitter](https://twitter.com/mirzaleka) to stay up to date with my content updates.

### Previous Chapter ⬅️

* [**Angular Reactive Forms Basics**](https://dev.to/mirzaleka/angular-reactive-forms-basics-3n5p)

Get [**Full Code**](https://gist.github.com/MirzaLeka/a4baf194d25c0cfb0243603efa5c7b87)

Bye for now 👋