Validating Angular Material Chips - Tags

Jonathan Gamble - Oct 24 '21 - - Dev Community

Validating tags in Angular Material does not work as it should out-of-the-box, so I am sharing my custom version. This assumes you are familiar with basic angular reactive forms, and are using Angular Material Chips to handle your tags input.

Here you will be able to produce min and required errors, as well as display them in mat-error as expected out-of-the-box. Unfortunately, Angular does not work this way out-of-the-box.

tags.service.ts

import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Injectable } from '@angular/core';
import { 
AbstractControl, 
FormArray, 
FormBuilder, 
ValidationErrors, 
ValidatorFn 
} from '@angular/forms';
import { 
MatChipInputEvent, 
MatChipList 
} from '@angular/material/chips';

export interface Tag {
  name: string;
}

@Injectable({
  providedIn: 'root'
})

/**
 * A bunch of tools for dealing with Tags
 */
export class TagService {

  visible = true;
  selectable = true;
  removable = true;
  addOnBlur = true;

  readonly separatorKeysCodes = [ENTER, COMMA];

  constructor(private fb: FormBuilder) { }

  /**
   * Get tags from form control
   * @param _tags tags
   */
  getTags(control: FormArray): string[] {
    let tags = [];
    for (let i = 0; i < control.length; ++i) {
      tags.push(control.at(i).value);
    }
    return tags;
  }

  /**
   * Adds an array of tags to a form control
   * @param tags
   * @param control
   */
  addTags(tags: any, control: FormArray): void {

    if (tags) {
      // add new tags
      for (let i = 0; i < tags.length; ++i) {
        this.addTag(this.tagFormat(tags[i]), control);
      }
    }
  }

  /**
   * Adds a tag to the form control
   * @param tag
   * @param control
   */
  addTag(tag: string, control: FormArray): void {
    control.push(this.fb.control(tag));
  }

  /**
   * Adds a tag to the form field
   * Necessary for MatChips
   * @param event - mat chip input event
   * @param tags - tags array
   */
  add(
    event: MatChipInputEvent, 
    control: FormArray, 
    chipList: MatChipList
  ): void {
    // add tag from keyboard
    const input = event.chipInput;
    const value = event.value;

    // Get rid of duplicates
    if (!(control.value as string[]).includes(value)) {

      // Add tag to new form group
      if (value.trim()) {
        const newVal = this.tagFormat(value);
        if (newVal) {
          this.addTag(newVal, control);
        }
      }
    }

    // Reset the input value
    if (input) {
      input.clear();
    }

    // update chip error state
    chipList.errorState = control.status === 'INVALID';
  }

  /**
   * Removes the tag from the form field
   * Necessary for MatChips
   * @param tag tag
   * @param tags - tags array
   */
  remove(
    index: number,
    control: FormArray,
    chipList: MatChipList
  ): void {

    control.removeAt(index);

    // update chip error state
    chipList.errorState = control.status === 'INVALID';
  }

  /**
   * Required Validator for tags
   * @param control
   * @returns
   */
  tagValidatorRequired(control: AbstractControl): 
    ValidationErrors | null {
    return (control.value && control.value.length === 0)
      ? { required: true }
      : null;
  };

  /**
   * Required Validator for tags
   * @param control
   * @returns
   */
  tagValidatorMin(min: number): ValidatorFn {
    return (control: AbstractControl): 
      ValidationErrors | null => {
      return (control.value && control.value.length > min)
        ? { min: true }
        : null;
    }
  };

  /**
   * Format a tag in db for viewing
   * @param tag
   * @returns
   */
  tagFormat(tag: string): string {
    // can't begin with number or contain only number, no dashes
    return this.slugify(tag)
    .replace(/-*/g, '').replace(/^\d+/, '');
  }

  /**
   * Create a slug from a string
   * @param value
   * @returns
   */
  slugify(value: string): string {
    return value
      .split('-').join(' ')
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .toLowerCase()
      .trim()
      .replace(/[^a-z0-9 ]/g, '') 
      .replace(/\s+/g, '-')
  }
}
Enter fullscreen mode Exit fullscreen mode

To use the validators, add the tag validators just like you add the other validators, but to a sub form group:

component constructor

public ts: TagService
Enter fullscreen mode Exit fullscreen mode

in your form creation of myForm

this.fb.group({
  title: ['', [
    Validators.required, 
    Validators.minLength(2)]
  ],
  content: ['', [
    Validators.required,
    Validators.minLength(3)]
  ],
  tags: this.fb.array([], [
    ts.tagValidatorMin(5), 
    ts.tagValidatorRequired])
});
Enter fullscreen mode Exit fullscreen mode

You can patch your tag values in just like anything else:

this.db.getFormData().pipe(
  tap((data: any) => {
    if (data) {
    ... // add other data

    // add tags
    const field = this.myForm.get('tags');
    this.ts.addTags(data.tags, field);

    ...
   }
 })
);
Enter fullscreen mode Exit fullscreen mode

and the component.html

<mat-form-field class="chip-list">
  <mat-chip-list #chipList aria-label="Tag selection">
    <mat-chip *ngFor="let tag of tagsField.controls; 
    let i = index" [selectable]="ts.selectable"
    [removable]="ts.removable" (removed)=
    "ts.remove(i, tagsField, chipList)">
      {{ tag.value }}
    <mat-icon matChipRemove *ngIf="ts.removable">
    cancel
    </mat-icon>
    </mat-chip>
    <input placeholder="Tags" [matChipInputFor]="chipList"
    [matChipInputSeparatorKeyCodes]="ts.separatorKeysCodes"
    [matChipInputAddOnBlur]="ts.addOnBlur" 
    (matChipInputTokenEnd)="ts.add($event, tagsField, chipList)"
    formArrayName="tags" />
  </mat-chip-list>
  <mat-error *ngIf="<!--check for errors here-->">
    <!-- errors are 'required' and 'min' -->
  </mat-error>
</mat-form-field>
Enter fullscreen mode Exit fullscreen mode

The service handles everything, so you don't have to think about chips, and it is reusable in multiple components. You can see you can easily modify the code for max or any other validator you can think of.

And you can actually use your tags form in Angular with errors.

J

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .