2
votes

I am using angular 9 mat-chips and I wish to know how can I stop adding new values in the input and only allow the the items to be added which are in autocomplete list i.e typing 'abc' which is not in the autocomplete list and pressing enter adds 'abc as a chip' in the input which needs to be avoided only values which are in the autocomplete list should be added. Also, I wish to know how can I stop adding duplicates in angular mat-chips i.e if I've already added lemon lemon should not get added to the mat-chips list and should get deleted from autocomplete list as well.

Following is the code:

chip-autocomplete.component.ts

@Component({
  selector: 'chips-autocomplete-example',
  templateUrl: 'chips-autocomplete-example.html',
  styleUrls: ['chips-autocomplete-example.css'],
})

export class ChipsAutocompleteExample {
  visible = true;
  selectable = true;
  removable = true;
  separatorKeysCodes: number[] = [ENTER, COMMA];
  fruitCtrl = new FormControl();
  filteredFruits: Observable<string[]>;
  fruits: string[] = ['Lemon'];
  allFruits: string[] = ['Apple', 'Lemon', 'Lime', 'Orange', 'Strawberry'];

  @ViewChild('fruitInput') fruitInput: ElementRef<HTMLInputElement>;
  @ViewChild('auto') matAutocomplete: MatAutocomplete;

  constructor() {
    this.filteredFruits = this.fruitCtrl.valueChanges.pipe(
        startWith(null),
        map((fruit: string | null) => fruit ? this._filter(fruit) : this.allFruits.slice()));
  }

  add(event: MatChipInputEvent): void {
    const input = event.input;
    const value = event.value;

    // Add our fruit
    if ((value || '').trim()) {
      this.fruits.push(value.trim());
    }

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

    this.fruitCtrl.setValue(null);
  }

  remove(fruit: string): void {
    const index = this.fruits.indexOf(fruit);

    if (index >= 0) {
      this.fruits.splice(index, 1);
    }
  }

  selected(event: MatAutocompleteSelectedEvent): void {
    this.fruits.push(event.option.viewValue);
    this.fruitInput.nativeElement.value = '';
    this.fruitCtrl.setValue(null);
  }

  private _filter(value: string): string[] {
    const filterValue = value.toLowerCase();

    return this.allFruits.filter(fruit => fruit.toLowerCase().indexOf(filterValue) === 0);
  }
}

chip-autocomplete.component.html

<mat-form-field class="example-chip-list">
  <mat-chip-list #chipList aria-label="Fruit selection">
    <mat-chip
      *ngFor="let fruit of fruits"
      [selectable]="selectable"
      [removable]="removable"
      (removed)="remove(fruit)">
      {{fruit}}
      <mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
    </mat-chip>
    <input
      placeholder="New fruit..."
      #fruitInput
      [formControl]="fruitCtrl"
      [matAutocomplete]="auto"
      [matChipInputFor]="chipList"
      [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
      (matChipInputTokenEnd)="add($event)">
  </mat-chip-list>
  <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
    <mat-option *ngFor="let fruit of filteredFruits | async" [value]="fruit">
      {{fruit}}
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

app.component.html

<div class="mat-app-background basic-container">
  <chips-autocomplete-example>loading</chips-autocomplete-example>
</div>

A stackblitz similar to this code(from angular material design) can be found at: https://stackblitz.com/angular/gdjdrkxaedv?file=src%2Fapp%2Fchips-autocomplete-example.ts

6

6 Answers

1
votes

To avoid duplicates change your selected function to below

selected(event: MatAutocompleteSelectedEvent): void {
    let index =   this.fruits.indexOf(event.option.viewValue);
     if(index == -1){
        this.fruits.push(event.option.viewValue);
      }
    this.fruitInput.nativeElement.value = '';
    this.fruitCtrl.setValue(null);
  }

1
votes

You can add a filter method to remove the duplicate entries on the dropdown list.

getUniqueList(fruitList: string[]) {
  return fruitList.filter(x => this.fruits.indexOf(x) === -1);
}

and all the filter method to your constructor.

constructor() {
this.filteredFruits = this.fruitCtrl.valueChanges.pipe(
  startWith(null),
  map((fruit: string | null) =>
    fruit
      ? this.getUniqueList(this._filter(fruit))
      : this.getUniqueList(this.allFruits.slice())
  )
);
}

then add an update to the remove method

remove(fruit: string): void {
const index = this.fruits.indexOf(fruit);

if (index >= 0) {
  this.fruits.splice(index, 1);
}
this.fruitCtrl.updateValueAndValidity();
}

To add unique items you can use the following code.

// Add our fruit
if ((value || "").trim()) {
  const filterList = this.getUniqueList(this.allFruits);
  const index = filterList.indexOf(event.value);
  if (index > -1) {
    this.fruits.push(value.trim());
  }
}

You can refer the updated code here.

1
votes

I am not sure am I understand right? But I create a minimal working example.

https://stackblitz.com/edit/angular-eooxih?file=src%2Fapp%2Fchips-autocomplete-example.html

Template

  <mat-form-field color="primary" style="width: 100%">
    <mat-label> Label</mat-label>
    <mat-select [formControl]="fruitCtrl" #multiSelect multiple>
      <mat-select-trigger>
        <mat-chip-list #chipList selected>
          <ng-container>
            <mat-chip color="primary" *ngFor="let fruit of fruitCtrl?.value; let matChipIndex = index" class="font-weight-normal" selected
              [removable]="true" (removed)="onRemoveFruit(multiSelect, matChipIndex)">
              {{fruit}}
              <mat-icon matChipRemove>cancel</mat-icon>
            </mat-chip>
          </ng-container>
        </mat-chip-list>
      </mat-select-trigger>
      <mat-option *ngFor="let item of allFruits" [value]="item">
        {{item}}
      </mat-option>
    </mat-select>
  </mat-form-field>

TS Imports

import { Component } from "@angular/core";
import { FormControl } from "@angular/forms";
import { MatSelect, MatSelectChange } from "@angular/material/select";

TS

export class Component {
  fruitCtrl = new FormControl([]);
  allFruits: string[] = ["Apple", "Lemon", "Lime", "Orange", "Strawberry"];

  constructor() {}

  onRemoveFruit(multiSelect: MatSelect, matChipIndex: number) {
    const selectedFruits = [...this.fruitCtrl.value];
    selectedFruits.splice(matChipIndex, 1);
    this.fruitCtrl.patchValue(selectedFruits);

    multiSelect.writeValue(selectedFruits);
  }
}
1
votes

In addition to prevent duplicates from being added to the list, you can also hide them in the dropdown list like this:

      <ng-container *ngFor="let fruit of filteredFruits | async">
        <mat-option *ngIf="!fruits.includes(fruit)" [value]="fruit">
          {{fruit}}
        </mat-option>
      </ng-container>
0
votes

You can check if the item is already present in the array and then add.

Try like this:

//update add(event: MatChipInputEvent)

if ((value || '').trim()) {
 if(!this.fruits.includes(value.trim())) { 
this.fruits.push(value.trim()); 
}
 }
0
votes

As stated by Lakshmi Narayan in his answer modifying selected function stops duplicates being added. Apart from this an additional check in add function stops adding values outside of autocomplete list as below:

select function

selected(event: MatAutocompleteSelectedEvent): void {
    let index =  this.fruits.indexOf(event.option.viewValue);
     if(index === -1){
        this.fruits.push(event.option.viewValue);
      }
    this.fruitInput.nativeElement.value = '';
    this.fruitCtrl.setValue(null);
  }

modified add function

add(event: MatChipInputEvent): void {
    const input = event.input;
    const value = event.value;
    if(this.allFruits.indexOf(value) !== -1){
      // Add our fruit
      if ((value || '').trim()) {
        this.fruits.push(value.trim());
      }
    }

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

    this.fruitCtrl.setValue(null);
  }