angulartypescriptangular-materialangular-reactive-formsangular-validation

Angular Material - Chips with Email validation


I'm trying to create a form with an input using chips to send a message to various emails. I have it almost functional but I don't know how to properly show the message error when you write an invalid email or don't write anything.

This is my custom validator:

import { FormControl } from '@angular/forms';

const REGEXP_EMAIL =
  /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;

export const emailsArrayValidator = (control: FormControl) => {
  const emails: string[] = Array.isArray(control.value) ? control.value : [];
  const invalidEmails = emails.filter(email => !REGEXP_EMAIL.test(email.trim()));
  return invalidEmails.length === 0 ? null : { invalidEmails: true };
};

This is the component.ts:

constructor(private fb: FormBuilder) {
  this.inviteForm = this.fb.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.pattern(this.REGEXP_EMAIL)]],
    date: ['', Validators.required],
    friends: [[], [Validators.required, Validators.minLength(1), emailsArrayValidator]],
  });
}

get name() {
  return this.inviteForm.get('name') as FormControl;
}

get email() {
  return this.inviteForm.get('email') as FormControl;
}

get date() {
  return this.inviteForm.get('date') as FormControl;
}

get friends() {
  return this.inviteForm.get('friends') as FormControl;
}

addFriend(event: MatChipInputEvent) {
  const friendEmail = event.value.trim();

  const friends = [...this.friends.value, friendEmail];
  const validationErrors = emailsArrayValidator(new FormControl(friends));
  console.log(validationErrors);
  if (validationErrors) {
    this.friendsErrorMessage = 'The last email is not valid';
  } else {
    this.friendsErrorMessage = '';
    this.friends.setValue(friends);
  }

  event.chipInput!.clear();
}

removeFriend(email: string) {
  const friends = this.friends.value.slice();
  const index = friends.indexOf(email);
  if (index !== -1) {
    friends.splice(index, 1);
    this.friends.setValue(friends);
  }
}

And this is the HTML:

<mat-form-field>
  <mat-label>Enter invitation email</mat-label>
  <mat-chip-grid #chipGrid>
    @for (friend of friends.value; track friend) {
      <mat-chip-row (removed)="removeFriend(friend)" [editable]="true">
        {{ friend }}
        <button matChipRemove>
          <mat-icon>cancel</mat-icon>
        </button>
      </mat-chip-row>
    }
    <input
      [matChipInputFor]="chipGrid"
      [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
      [matChipInputAddOnBlur]="addOnBlur"
      (matChipInputTokenEnd)="addFriend($event)"
      required
    /></mat-chip-grid>
    @if (friends.errors && friends.touched) {
      <mat-error>{{ friendsErrorMessage }}</mat-error>
    }
</mat-form-field>

Solution

  • Your issue is similar to this GitHub issue: mat-error with MatChipInput is not displayed under the input.

    1. Trigger errorState = true to show the error message.
    2. Set the error for friends control via <AbstractControl>.setErrors(/* ValidationError */).
    3. Mark the friends control as touched via <AbstractControl>.markAsTouched() to display the error message as your condition: @if (friends.errors && friends.touched).

    Besides, you should also reset the errorState and remove the validation error for the scenario where the invalid email is inputted previously and then fill in the valid email.

    import {MatChipGrid} from '@angular/material/chips';
    
    @ViewChild('chipGrid') chipGrid: MatChipGrid;
    
    addFriend(event: MatChipInputEvent) {
      const friendEmail = event.value.trim();
    
      const friends = [...this.friends.value, friendEmail];
      const validationErrors = emailsArrayValidator(new FormControl(friends));
      console.log(validationErrors);
      if (validationErrors) {
        this.friendsErrorMessage = 'The last email is not valid';
        this.friends.setErrors(validationErrors, { emitEvent: false })
        this.chipGrid.errorState = true;
      } else {
        this.chipGrid.errorState = false;
        this.friends.setErrors({ invalidEmails: null });
    
        this.friendsErrorMessage = '';
        this.friends.setValue(friends);
      }
    
      this.friends.markAsTouched();
      event.chipInput!.clear();
    }
    

    Demo @ StackBlitz