Skip to content

Instantly share code, notes, and snippets.

@MirzaLeka
Last active July 18, 2023 19:27
Show Gist options
  • Save MirzaLeka/156679b0eaf0246856fe3915de75ebce to your computer and use it in GitHub Desktop.
Save MirzaLeka/156679b0eaf0246856fe3915de75ebce to your computer and use it in GitHub Desktop.
Angular form input validaiton

Validations & Errors in Angular Reactive Forms

This document will teach you how to validate from control in Angular using Reactive Forms. Start by creating a simple form:

Step 1: Go to component module and import ReactiveFormsModule

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

Step 2: Set up form using FormBuilder

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 {
    }

}

Step 3: Add form in the HTML template

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

Step 4: Add validations in the component

  private setupForm(): void {
    this.todoForm = this.fb.group({
      [FormFields.Title]: ['', [
        Validators.required, // field is mandatory
        Validators.maxLength(100), // field must not exceed 100 characters
        Validators.pattern(new RegExp('^\\p{L}+$', 'u'))] // field must follow this pattern (only letters allowed)
      ],
      [FormFields.Description]: ['', [Validators.maxLength(300)]]
    });
  }

Create getter to retrieve from control that you'll use in the template

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

Apply validation checks in template

  <div class="title-wrapper">
    <input formControlName="title" type="text" />
    <div class="error-message" *ngIf="titleControl.invalid && (titleControl.dirty || titleControl.touched)">
      <div *ngIf="titleControl.hasError('required')">String is required</div>
      <div *ngIf="titleControl.hasError('maxlength')">String is too long</div>
      <div *ngIf="titleControl.hasError('pattern')">String contains numbers, symbols or spaces</div>
    </div>
  </div>

Clarifications

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

  • If control validation failed, control.invalid will be true
  • If control is dirty or touched means user either clicked or started typing on input
  • You want to display validation error when form is invalid only after user interacted with the field
      <div *ngIf="titleControl.hasError('required')">String is required</div>
      <div *ngIf="titleControl.hasError('maxlength')">String is too long</div>
      <div *ngIf="titleControl.hasError('pattern')">String contains numbers, symbols or spaces</div>

These three display an error for each validation parameter that was set in the component

Step 5: Validate after user stopped typing

Create a dictionary-like structure that holds form fields.

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

Create a function that will toggle validation rules when user starts and stops typing

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

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

Call function above in ngOnInit for each form field

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

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

Apply this in the template

  <div class="title-wrapper">
    <input formControlName="title" type="text" />
    <div class="error-message" *ngIf="formFieldCanBeValidated['title'] && titleControl.invalid && (titleControl.dirty || titleControl.touched)">
      <div *ngIf="titleControl.hasError('required')">String is required</div>
      <div *ngIf="titleControl.hasError('maxlength')">String is too long</div>
      <div *ngIf="titleControl.hasError('pattern')">String contains numbers, symbols or spaces</div>
    </div>
  </div>

Now the validation message will be displayed only after you stop typing (debounce expires).

Step 6: Unsubscribe from the field value changes Observable

Using the previously created unsubscribed$ Subject, implement OnDestroy hook and use it to terminate the Subject.

export class TodoFormComponent implements OnInit, OnDestroy 

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

Step 7: Prohibit form submit if form is not valid

This can be done either in template or the component. Here is how you apply it in the component:

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

You can read form values using .value flag. This returns an object, where each field is key and value is field value.

console.log(this.todoForm.value)
// {title: 'Hello', description: 'World'}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment