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, '-')
}
}
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
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])
});
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);
...
}
})
);
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>
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