0

I'm trying to create a simple form with a dynamic number of mat-autocomplete fields. However, in some cases, the inputs' displayed values get lost.

Component's script

export class AutocompleteSimpleExample {
  availableItems = [{name: 'item1'}, {name: 'item2'}];
  items = [{name: 'item1'}, {}];

  addItem() {
    this.items.push({});
  }

  deleteItem(i: number) {
    this.items.splice(i, 1);
  }

  displayObject(obj) {
    return obj ? obj.name : null;
  }
}

Component's view

<form>
  <mat-form-field *ngFor="let item of items; let i = index">
    <input matInput [(ngModel)]="items[i]" [matAutocomplete]="itemAutocomplete" name="items[{{i}}]">

    <mat-autocomplete #itemAutocomplete="matAutocomplete" [displayWith]="displayObject">
      <mat-option *ngFor="let item of availableItems" [value]="item">{{item.name}}</mat-option>
    </mat-autocomplete>

    <button mat-button mat-icon-button matSuffix type="button" (click)="deleteItem(i)"><mat-icon>close</mat-icon></button>
  </mat-form-field>

  <button class="btnType01" mat-raised-button type="button" (click)="addItem()"><mat-icon>add</mat-icon>Add Item</button>
</form>

I've made a StackBlitz to showcase the issue. If you:

  1. Select an item in the second autocomplete field (e.g. Item 2)
  2. Remove the item from the first autocomplete field (using the x at the end of the field)
  3. Click Add Item to add another one

The first autocomplete field will then show an empty value, instead of keeping the one it had.

Could anyone help me figure out why the value is lost? Is this the wrong way of dealing with a dynamic number of autocomplete fields?

Jeto
  • 14,596
  • 2
  • 32
  • 46

1 Answers1

2

angular can’t keep track of items in the array and has no knowledge of which items have been removed or added.

As a result, Angular needs to remove all the DOM elements that associated with the data and create them again. That means a lot of DOM manipulations.

but you can try with a custom trackby :

<form>
    <mat-form-field *ngFor="let item of items; let i = index; trackBy:customTrackBy">
        <input matInput [(ngModel)]="items[i]" [matAutocomplete]="itemAutocomplete"
                     name="items[{{i}}]">

    <mat-autocomplete #itemAutocomplete="matAutocomplete" [displayWith]="displayObject">
            <mat-option *ngFor="let item of availableItems" [value]="item">{{item.name}}      </mat-option>
        </mat-autocomplete>

    <button mat-button mat-icon-button matSuffix type="button" (click)="deleteItem(i)"><mat-icon>close</mat-icon></button>
  </mat-form-field>

  <button class="btnType01" mat-raised-button type="button" (click)="addItem()"><mat-icon>add</mat-icon>Add Item</button>


</form>

ts:

customTrackBy(index: number, obj: any): any {
    return  index;
}

DEMO

Fateme Fazli
  • 11,582
  • 2
  • 31
  • 48
  • That does seem to do the trick, even though I'm not sure to follow. `ngModel` is bound to `items[i]`, which is `this.items[i]` (a component's property) and not `item` (the local variable that I'm not using). I must run right now, I'll probably accept your answer later (though I'd love to understand exactly what difference it makes). Thanks! – Jeto Oct 21 '18 at 10:54
  • @Jeto The trackBy function takes the index and the current item as arguments and needs to return the unique identifier for this item.so Angular can track which items have been added or removed according to the unique identifier and create or destroy only the things that changed. – Fateme Fazli Oct 21 '18 at 11:03
  • and yes it's not about local variable you did it right, i misunderstood. – Fateme Fazli Oct 21 '18 at 11:07
  • Alright, I'll make sure to get more familiar with `trackBy` then (I'm still a tiny bit confused as to why it can't work properly without it). Thanks again for your help! – Jeto Oct 21 '18 at 17:00