Angular mat-select binded to array's elements by ngFor's index property causes undesirable behaviour

Issue

Problematic code ts part:

    import { Component } from '@angular/core';
    import { FormControl } from '@angular/forms';
    
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html'
    })
    export class AppComponent {
      constructor() {}
      fields = [null, null];
      countries: any = [
        {
          full: 'Great Britain',
          short: 'GB'
        },
        {
          full: 'United States',
          short: 'US'
        },
        {
          full: 'Canada',
          short: 'CA'
        }
      ];
    }

Problematic code html part:

    <div *ngFor="let f of fields; let i = index">
      <mat-form-field>
        <mat-select name="countryString" [(ngModel)]="fields[i]" placeholder="Country">
          <mat-option *ngFor="let item of countries" [value]="item.short">{{item.full}}</mat-option>
        </mat-select>
      </mat-form-field>
    </div>

Here is my example on stackblitz.

The basic idea is the following:

  • there is an array of strings
  • initially the array consist fix number of elements, but with null
    value
  • we want to give value to the elements, so we iterate through them,
    and bind them to a mat select component

The problem starts when we want to select the first element, because when you selet it, the second component takes the property too. The interesting part is, that the second element of the array dos not get the value but the binded mat-select component switches to that element.

As you can see I wrote a working version without the ngFor part and it works that way like a charm, so my question is what can be the issue here? Is it a material design flaw or is it me who makes some mistake?

Solution

This occurs because ngFor sees the item in the array as the same and is comparing them to find which template it should replace. Therefore, when the values are the same, the directive stamps out the new template for the selected value and replaces both occurrences, because the value is the only thing it has to compare.
You can try using an object in the array so the template is not re-rendered like so:

  fields = [{
    value: null
  }, {
    value: null
  }];
<div *ngFor="let f of fields; let i = index">
  <mat-form-field>
    <mat-select name="countryString" [(ngModel)]="fields[i].value" placeholder="Country">
      <mat-option *ngFor="let item of countries" [value]="item.short">{{item.full}}</mat-option>
    </mat-select>
  </mat-form-field>
</div>

This way the object in the ngFor is always the same and avoids a rerendering of the template, but allows you to change a specific value at that index.

Or use trackBy and use the index to track the templates:

  trackBy(index, val) {
    return index
  }
<div *ngFor="let f of fields; let i = index; trackBy: trackBy">

Example here

Answered By – shteeven

Answer Checked By – Dawn Plyler (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.