Mat-Error Required validation error message shows when form is first open

Viewed 141

I have a custom control component that is used to show a required validation error for mat-chip-lists. On the form that I'm using this component, when I first open this form the required validation message shows. I only want it to show when the field is not populated with any data. Can someone please provide a reason why and a solution to what I need to do to get the validation to work properly.

I could be overcomplicating this and it's much simpler than I thought.

html where mat-error element for required show:

<mat-form-field [floatLabel]="floatLabel">
 <mat-label>{{ label }}</mat-label>
  <mat-chip-list #optionList aria-label="label" required>
   <mat-chip *ngFor="let item of selectedOptions" (removed)="removed(item)" 
 [removable]="!item.disabled" [disabled]="item.disabled">
  {{ item.text }}
  <mat-icon *ngIf="!item.disabled" matChipRemove>cancel</mat-icon>
</mat-chip>
  <input
  #optionInput
  type="text"
  [placeholder]="placeholder"
  [formControl]="formControl"
  [matAutocomplete]="optionAutoComplete"
  [matChipInputFor]="optionList"
  [required]="required"
  />
</mat-chip-list>
<mat-autocomplete #optionAutoComplete="matAutocomplete" 
 (optionSelected)="selected($event.option.value)">
  <mat-option *ngFor="let option of filteredOptions | async" [value]="option">
  {{ option.text }}
  </mat-option>
 </mat-autocomplete>
 <mat-hint *ngIf="hint">{{ hint }}</mat-hint>
 </mat-form-field>
 <mat-error *ngIf="required === true && hasValue === false && 
        isInitialized === true"> {{ label }} is 
 <strong>required</strong> </mat-error>

Typescript for the custom control The removed function fires when you remove selected items from the list. It turns the check back on and sends the update to the parent component.

 Component({
  selector: 'app-chips',
  templateUrl: './chips.component.html',
 })
 export class ChipsComponent implements OnInit, DoCheck {
  @Input() label = '';
  @Input() placeholder = '';
  @Input() options: Options[] = [];
  @Input() selectedOptions: Options[] = [];
  @Input() floatLabel: FloatLabelType = 'auto';
  @Input() hint!: string | undefined;
  @Input() required = true;
  hasValue: boolean = this.selectedOptions.length > 0;
  isInitialized: boolean | undefined;

 @ViewChild('optionInput') optionInput: ElementRef | undefined;

 @Output() onRemoved = new EventEmitter<Options>();
 @Output() selectedOptionsChanged = new EventEmitter<Options[]>();

formControl = new FormControl('');
filteredOptions: Observable<Options[]> | undefined;

iterableDiffer: IterableDiffer<Options>;

constructor(private readonly iterableDiffers: IterableDiffers) {
   this.iterableDiffer = this.iterableDiffers.find([]).create();
 }

 ngDoCheck(): void {
  const optionChanges = this.iterableDiffer.diff(this.options);
   if (optionChanges) {
    this.filteredOptions = of(this.options);
   }
   if (this.required === undefined) {
    this.required = false;
   }
 }

 ngOnInit(): void {
   this.subscribeFilterOptions();
   this.isInitialized = true;
}

    ngAfterViewInit(): void {
     this.isInitialized = true;
     }

 selected(value: Options): void {
  if (this.optionInput) {
    this.optionInput.nativeElement.value = '';
   }
   if (!this.selectedOptions.find((x) => x.text === value.text)) {
    this.selectedOptions.push(value);
    this.selectedOptionsChanged.emit(this.selectedOptions);
  }
  this.hasValue = this.selectedOptions.length > 0;
}

private subscribeFilterOptions() {
  this.filteredOptions = this.formControl.valueChanges.pipe(
    startWith(''),
    map((value: string | Options) =>
    value && typeof value === 'string' ? this.options.filter((o) => 
 o.text.toLowerCase().includes(value.toLowerCase())) : 
this.options.slice()
   )
  );
}
removed(value: Options): void {
  this.onRemoved.emit(value);
  this.hasValue = this.selectedOptions.length > 0;
  }
 }

Mat-chip-list component on the form

<div class="col-md-12">
  <app-linq-chips
    label="Entities"
    placeholder="Add Entity..."
    [options]="entityOptions"
    [selectedOptions]="selectedEntities"
    [hint]="
      entityListHasDisabledOptions === true
        ? 'Please remove this contact from any roles for an entity prior to removing their 
     association to that entity.'
        : undefined
    "
    (onRemoved)="removeEntity($event)"
  >
  </app-linq-chips>
</div>
1 Answers

Your variable hasValue is only set when a chip is selected or removed. When the component is initialized it's value is false as the number of selected options is zero.

hasValue: boolean = this.selectedOptions.length > 0;

The required variable is initialized to true, so the mat error condition below:

<mat-error *ngIf="required === true && hasValue === false"> {{ label }} is 
 <strong>required</strong> </mat-error>

will be true, so the error will show initially.

To fix this issue, add an extra boolean variable, isInitialized (or whatever you want to name it), set it to false in the ngOnInit(), then set it to true in the ngAfterViewinit().

Update your mat-error condition like follows:

<mat-error *ngIf="required === true && hasValue === false && isInitialized === true"> {{ label }} is 
 <strong>required</strong> </mat-error>

Try the above and it should only error check after the component selections are made.

Related