Angular.js: <input> loses focus when ng-model references ng-repeat array directly

Created on 18 Nov 2015  ·  10Comments  ·  Source: angular/angular.js

I have a simple <input> inside a <div ng-repeat...">

As soon as I type a character the <input> focus is lost. To continue typing I first have to click inside the input area for _each_ character I want to enter.

The essence of the ng-repeat code that shows the problem is:

<div ng-repeat="item in items">
    <input ng-model="items[$index]">
</div>

However, an <input> inside the ng-repeat scope but which does not directly reference the ng-repeat array, works fine - I can enter data all day long. For example:

<div ng-repeat="item in items">
    <input ng-model="george">
</div>

Note: the problem is clearly related to the the ng-model referring directly to the $index-th element in the items array. My guess is that a $watch on the collection is triggered and that causes the loss of focus.

Here's a codepen that shows the problem:
http://codepen.io/djmarcus/pen/bVZGvj

Here's a distillation:

    <div ng-controller="myCtrl">
        <div ng-repeat="item in items">
          <div>This fails</div>
          <div layout="row" layout-align="start center">
            <input ng-model="items[$index]"  label=""> 
          </div>
        </div>

        <div ng-repeat="item in items">
          <div>This works</div>
          <div layout="row" layout-align="start center">
            <input ng-model="george"  label=""> 
          </div>
        </div>

        <div>This works (its outside of ng-repeat) </div>
        <input ng-model="harvey" label="">
    </div>
ngRepeat investigation

Most helpful comment

I think this is expected behaviour.

As @Narretz mentioned, your 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).

There are several ways to achieve the desired behaviour:

  1. Use the local item variable created in ngRepeat's child scope: Actually, not working for 2-way binding in this case. Use options 2 or 3.

html <div ng-repeat="item in items"> <input type="text" ng-model="item" /> </div>

  1. Use track by $index (if it's appropriate for your usecase, i.e. you know the items won't be reordered):

html <div ng-repeat="item in items track by $index"> <input type="text" ng-model="items[$index]" /> </div>

  1. Wrap your strings into objects. E.g. items = [{value: 'string1'}, {value: 'string2'}, ...]:

html <div ng-repeat="item in items"> <input type="text" ng-model="items[$index].value" /> </div>

You can see all 3 solutions in action in this demo pen.

All 10 comments

I think there's a related issue floating around somewhere, but that was because the repeated elements were moved, which caused them to lose the focus. Since in this example, there should happen no DOM reordering, this needs an investigation.

I think this is expected behaviour.

As @Narretz mentioned, your 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).

There are several ways to achieve the desired behaviour:

  1. Use the local item variable created in ngRepeat's child scope: Actually, not working for 2-way binding in this case. Use options 2 or 3.

html <div ng-repeat="item in items"> <input type="text" ng-model="item" /> </div>

  1. Use track by $index (if it's appropriate for your usecase, i.e. you know the items won't be reordered):

html <div ng-repeat="item in items track by $index"> <input type="text" ng-model="items[$index]" /> </div>

  1. Wrap your strings into objects. E.g. items = [{value: 'string1'}, {value: 'string2'}, ...]:

html <div ng-repeat="item in items"> <input type="text" ng-model="items[$index].value" /> </div>

You can see all 3 solutions in action in this demo pen.

Thanks @gkalpak. You are right, the behavior is expected.

I ended up using option #3. Thanks for clarifying the issue (in hindsight its obvious).

It would help if there was some documentation on Angular 'gotcha' issues.. this one is a prime candidate for that list.

TBH, I would expect you'd follow option #1. It's quite typical to use it like this - I wonder why can't you use it (if there's a specific reason).

Option #1 (as coded in the codepen) is what I was using originally. It does not work (at least when I try it in the codepen) for the reasons you gave above... perhaps you meant something different in the codepen?

Nevertheless, option #3 is most closely related to my code. The array that I ng-repeat on actually is an array of objects (not simple strings) where my interest is in one of the properties.. so option #3 is a good match.

[Option #1] does not work (at least when I try it in the codepen)

Ooops, you are right. Option #1 is not suitable for 2-way binding via ngModel.
(I updated my previous comment to avoid confusion for future readers.)

approach #2 by @gkalpak fixed the problem. I needed this in Angular 2 though. Thanks for the solution.

If you stumbled upon this thread and are looking for a solution in Angular 2, the solution is quite simple.
Instead of two way binding with the array, only bind the property and handle the change event using 'change' event instead of 'input'

Example:

// Instead of writing
<div *ngFor="let param of params.value; let id = index;">
    <input type="text" placeholder="0" [(ngModel)]="params.value[id]">
</div

// Write
<div *ngFor="let param of params.value; let id = index;">
    <input type="text" placeholder="0" [ngModel]="params.value[id]" (change)="onValueUpdate($event, id)">
</div>

and update the value in onValueUpdate function.

Since the change event triggers only when you blur from input, your will not face the error anymore.

thanks @gkalpak
it worked for me. I ended up with option3. it worked for angular 4 also

Was this page helpful?
0 / 5 - 0 ratings