Create custom validators in Angular application

Home » Tutorials » JavaScript » Create custom validators in Angular application
In this video, we’ll see how to add custom sync and async validators in an Angular application, and also how to handle Observable unsubscription.
First, we will add an Observable unsubscription mechanism. This is useful in any application to avoid memory leaks. In modern Angular, there is a takeUntilDestroyed() operator. It takes an instance of the built-in DestroyRef class as a parameter. This class allows you to register callbacks that are triggered when a component (or directive) is destroyed.

We will also add validators. In Angular, validators can be synchronous or asynchronous. The framework provides several built-in synchronous validators (based on HTML5 validation). A validator in Angular is a function (ValidatorFn) that takes a control (AbstractControl) and returns either null or a ValidationErrors object.

common-validators.ts

import { AbstractControl, ValidationErrors } from '@angular/forms';

export class CommonValidators {
  static preventEmptyValue(control: AbstractControl): ValidationErrors | null {
    return control.value.trim() ? null : { emptyValue: true };
  }
}

is-unuqiue-post-title.ts

import { map, Observable, of } from 'rxjs';
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { inject, Injectable } from '@angular/core';
import { PostService } from '../../features/posts/services/post-service';

@Injectable({ providedIn: 'root' })
export class IsUnuqiuePostTitleValidator implements AsyncValidator {
  private postService = inject(PostService);

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    const postTitle = control.value.trim();
    if (!postTitle) {
      return of(null);
    }
    return this.postService
      .checkPostTitle(postTitle)
      .pipe(map((result) => (result.length ? { titleExists: true } : null)));
  }
}

post-service.ts

import { HttpClient, HttpParams } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class PostService {
  private readonly api = 'https://jsonplaceholder.typicode.com';

  private http = inject(HttpClient);

  createPost(title: string, body: string, userId: number): Observable<void> {
    return this.http.post<void>(`${this.api}/posts`, { title, body, userId });
  }

  checkPostTitle(title: string): Observable<any> {
    const params = new HttpParams().set('title', title);
    return this.http.get<any>(`${this.api}/posts`, {params});
  }
}

post-form.ts

import { Component, DestroyRef, inject, Input, OnInit } from '@angular/core';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import {
  FormBuilder,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { PostService } from '../../../../../posts/services/post-service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonValidators } from '../../../../../../core/validators/common-validators';
import { IsUnuqiuePostTitleValidator } from '../../../../../../core/validators/is-unuqiue-post-title';

interface PostFormGroup {
  postTitle: FormControl<string>;
  postContent: FormControl<string>;
}

@Component({
  selector: 'app-post-form',
  imports: [MatFormFieldModule, MatInputModule, MatButtonModule, ReactiveFormsModule],
  templateUrl: './post-form.html',
  styleUrl: './post-form.scss',
})
export class PostForm implements OnInit {
  protected form!: FormGroup<PostFormGroup>;

  private destroyRef = inject(DestroyRef);
  private isUniquePostTitleValidator = inject(IsUnuqiuePostTitleValidator);

  @Input() userId = 0;

  constructor(
    private formBuilder: FormBuilder,
    private postService: PostService,
  ) {}

  ngOnInit(): void {
    this.form = this.formBuilder.group({
      postTitle: new FormControl<string>('', {
        nonNullable: true,
        validators: [Validators.required, CommonValidators.preventEmptyValue],
        asyncValidators: [
          this.isUniquePostTitleValidator.validate.bind(this.isUniquePostTitleValidator),
        ],
        updateOn: 'blur',
      }),
      postContent: new FormControl<string>('', {
        nonNullable: true,
        validators: [Validators.required],
      }),
    });
  }

  protected createPost() {
    if (this.form.invalid || !this.userId) {
      return;
    }
    const formValue = this.form.getRawValue();
    this.postService
      .createPost(formValue.postTitle, formValue.postContent, this.userId)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe();
  }
}

post-form.html

<h1>Create new post</h1>

<form [formGroup]="form" (ngSubmit)="createPost()">
  <div>
    <mat-form-field>
      <mat-label>Enter post title</mat-label>
      <input matInput placeholder="PostService title" formControlName="postTitle" />
      @if (form.controls.postTitle.hasError('emptyValue')) {
        <mat-error>Invalid value</mat-error>
      }

      @if (form.controls.postTitle.hasError('titleExists')) {
        <mat-error>Title is not unique</mat-error>
      }
    </mat-form-field>
  </div>

  <mat-form-field class="example-full-width">
    <mat-label>Enter post content</mat-label>
    <textarea matInput placeholder="PostService content" formControlName="postContent"></textarea>
  </mat-form-field>

  <div>
    <button [disabled]="form.invalid" matButton="filled">Save</button>
  </div>
</form>

0 Comments

Submit a Comment

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


The reCAPTCHA verification period has expired. Please reload the page.

Pin It on Pinterest

Share This