70

I'm making a dynamic form. A Field has a list of values. Each value is represented by a string.

export class Field{
    name: string;
    values: string[] = [];
    fieldType: string;
    constructor(fieldType: string) {this.fieldType = fieldType;}
}

I have a function in my component which adds a new value to the field.

addValue(field){
    field.values.push("");
}

The values and the button are displayed like this in my HTML.

<div id="dropdown-values" *ngFor="let value of field.values; let j=index">
    <input type="text" class="form-control" [(ngModel)]="field.values[j]" [name]="'value' + j + '.' + i"/><br/>
</div>
<div class="text-center">
    <a href="javascript:void(0);" (click)="addValue(field)"><i class="fa fa-plus-circle" aria-hidden="true"></i></a>
</div>

As soon as I write some text in an input of a value, the input loses focus. If I add many values to a field and I write a character in one the values input, the input lose focus and the character is written in every input.

dhilt
  • 18,707
  • 8
  • 70
  • 85
Elbbard
  • 2,064
  • 6
  • 30
  • 53
  • Possible duplicate of [how to use track by inside ngFor angular 2](https://stackoverflow.com/questions/36322829/how-to-use-track-by-inside-ngfor-angular-2) – asdasd May 14 '19 at 10:23

3 Answers3

174

This happens when the array is a primitive type, in your case a String array. This can be solved by using TrackBy. So change your template to match the following:

<div *ngFor="let value of field.values; let i=index; trackBy:trackByFn">
    <input type="text" [(ngModel)]="field.values[i]"  /><br/>
</div>
<div>
    <button (click)="addValue(field)">Click</button>
</div>

and in the ts file add the function trackByFn, which returns the (unique) index of the value:

trackByFn(index: any, item: any) {
   return index;
}

This is a link about the same issue, except the issue is for AngularJS, but the problem corresponds yours. Most important excerpt from that page:

You are repeating over an array and you are changing the items of the array (note that your items are strings, which are primitives in JS and thus compared "by value"). Since new items are detected, old elements are removed from the DOM and new ones are created (which obviously don't get focus).

With TrackBy Angular can track which items have been added (or removed) according to the unique identifier and create or destroy only the things that changed which means you don't lose focus on your input field :)

As seen in the link you can also modify your array to contain objects which are unique and use [(ngModel)]="value.id" for example, but that's maybe not what you need.

AT82
  • 71,416
  • 24
  • 140
  • 167
  • 4
    In my case, I was not iterating over an array of primitives, but an array of objects (a FormArray, in fact) and I still got the same problem. Even that, I followed @AJT_82's suggestion and it solved the problem – Thisisalexis Nov 01 '17 at 15:25
  • 1
    @AlexisJoséBravoLlovera Glad to hear it worked and was helpful to you! There seems to be some kind of thing going on with formarray, don't know the reason myself for that, since we are indeed dealing with objects, similar issue: https://stackoverflow.com/questions/44445676/add-to-the-the-beginning-of-reactive-forms-array-angular – AT82 Nov 01 '17 at 15:30
  • you sir are a hero. Thanks. – java-addict301 Oct 01 '19 at 13:14
  • 1
    @AJT82 thanks a lot ! this helped me so much i was struggling so hard – AOUADI Slim Jul 08 '20 at 22:15
1

This was happening to me when I was iterating over an object's keys and values by using a helper function:

<div *ngFor="let thing of getThings()" [attr.thingname]="thing.key">
  ... {{ applyThing(thing.value) }}
</div>

In my component I was returning an array of objects containing key/value pairs:

export ThingComponent {
  ...

  //this.things = { a: { ... }, b: { ... }, c: { ... } }

  public getThings() {
    return Object.keys(this.things).map((key) => {
      return {key: key, value: this.things[key] }
    })
  }
}

The answer given by @AJT_82 definitely works exactly as advertised. However in my case the specific issue was that the helper function, getThings(), was returning a new list of objects every time. Even though their content was the same, the objects themselves were regenerated on every call to the function (which was happening during change detection) and hence, to the change detector, they had different identities, and the form was regenerated on every model change.

The simple solution in my case was to cache result of getThings() and use that as the iterator:

<div *ngFor="let thing of cachedThings" [attr.thingname]="thing.key">
  ... {{ applyThing(thing.value) }}
</div>

...

export ThingComponent {
  public cachedThings = getThings()
  ...

  //this.things = { a: { ... }, b: { ... }, c: { ... } }

  private getThings() {
    return Object.keys(this.things).map((key) => {
      return {key: key, value: this.things[key] }
    })
  }
}

In cases where cachedThings might need to vary, it'll need to be updated manually so the change detector will trigger re-rendering.

t.888
  • 3,862
  • 3
  • 25
  • 31
0

When you input some text the angular ngOnChange listener will run, so the ngFor indexes will rerendered and the focus will be lost. For avoid this issue a trackFn can be added to the ngFor. See https://angular.io/api/core/TrackByFunction for more details on the topic. Below the code solution for your issue:

In you html code define the ngFor a trackFn function that will be corrispond to the trackByFn function declared in the .ts file.

    <div *ngFor="let value of field.values; let i=index; trackBy:trackByFn">
    <input type="text" [(ngModel)]="field.values[i]"  /><br/>
</div>
<div>
    <button (click)="addValue(field)">Click</button>
</div>

And in the ts file declare the abowe mentioned trackByFn function:

trackByFn(i: number, items: any) {
return index //returning the index itself for avoiding ngFor to change focus after ngModelChange
}
vnapastiuk
  • 609
  • 7
  • 12