Custom Form Validation With Angular

The basic concept behind creating custom form validation is to attach a custom directive to an input. In my example, I am validating a text input to ensure that there no forbidden words or characters are in my file name. Below is an example of my custom validation directive. I am only checking a few values, but the list of invalid words or characters can become a bit lengthy.

validate-filename.directive.ts

import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';

@Directive({
selector: '[validateFilename]',
providers: [ { provide: NG_VALIDATORS, useExisting: ValidateName, multi: true } ]
})
export class ValidateFilenameDirective implements Validator {
private readonly INVALID_WORD = ‘derp’;
private readonly INVALID_CHAR = ‘$’;


  validate(control: AbstractControl): ValidationErrors {
    if (!control.value) {
      return null;
    }
  
    let fileName = control.value as string;
  
    if (fileName.includes(this.INVALID_CHAR)) {
      return { fileNameInvalidChar: { value: true } };
    }
  
    if (fileName.includes(this.INVALID_WORD)) {
      return { fileNameInvalidWord: { value: true } };
    }
    return null;
  }
}

As you can see above, my validate method will return null if my if my string does not contain any errors. Otherwise, if my string contains errors, I can return a ValidationErrors object containing a custom validation error. This validation directive has the ability to return several different types of validation errors, to conditionally display the correct error message to my user. This implementation can be seen in the main.component.html example. I then went ahead and added my custom directive to my module like the example below.

shared.module.ts

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

@NgModule({
imports:      [BrowserModule, BrowserAnimationsModule],
exports: [ValidateFilenameDirective],
declarations: [ValidateFilenameDirective]
})
export class SharedModule {}

I can then attach my custom validation message to my html template form. As you can see below, the directive will mark the form as invalid, and you can then check what errors are on your form by saying fileName?.errors.<my error>. This will allow you to display your customized message.

main.component.html

<form #form="ngForm">
  <label for="fileName">File Name</label>
  <input #fileName="ngModel" id="fileName" name="fileName" [(ngModel)]="filename" validateFilename maxlength="255">

   <div *ngIf="fileName.invalid && fileName.dirty">
    <div *ngIf="fileName?.errors.fileNameInvalidChar">
      <span>Filename cannot include: $</span>
    </div>
    <div *ngIf="fileName?.errors.fileNameInvalidWord">
     <span>Filename cannot include reserved word: DERP</span>
    </div>
  </div>
</form>

Dynamic Form Validation

This example is great for when you have a very static set of rules to apply to your input field. However, you might quickly accumulate a larger quantity of custom validators.

You could make this validation directive more dynamic by passing in a regular expression. This is a much more powerful technique to validate your input. Refer to the brief example below.

validate-name.directive.ts

import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';

@Directive({
selector: '[validateName]',
providers: [ { provide: NG_VALIDATORS, useExisting: ValidateNameDirective, multi: true } ]
})
export class ValidateNameDirective implements Validator {
  @Input() regex: RegExp;
  
  validate(control: AbstractControl): ValidationErrors {
  // Validate your input here
  }
}

When attaching this directive to your view, remember to surround your directive with square brackets, and then pass in your regular expression from your component file.

<input #fileName="ngModel" id="fileName" name="fileName" [(ngModel)]="filename" [validateFilename]=“validationRegEx” maxlength="255">