117

I am trying to allow the user to edit a list of items by using ngRepeat and ngModel. (See this fiddle.) However, both approaches I've tried lead to bizarre behavior: one doesn't update the model, and the other blurs the form on each keydown.

Am I doing something wrong here? Is this not a supported use case?

Here is the code from the fiddle, copied for convenience:

<html ng-app>
    <head>
        <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.1/css/bootstrap-combined.min.css" rel="stylesheet">
    </head>
    <body ng-init="names = ['Sam', 'Harry', 'Sally']">
        <h1>Fun with Fields and ngModel</h1>
        <p>names: {{names}}</p>
        <h3>Binding to each element directly:</h3>
        <div ng-repeat="name in names">
            Value: {{name}}
            <input ng-model="name">                         
        </div>
        <p class="muted">The binding does not appear to be working: the value in the model is not changed.</p>
        <h3>Indexing into the array:</h3>
        <div ng-repeat="name in names">
            Value: {{names[$index]}}
            <input ng-model="names[$index]">                         
        </div>
        <p class="muted">Type one character, and the input field loses focus. However, the binding appears to be working correctly.</p>
    </body>
</html>

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
Nick Heiner
  • 119,074
  • 188
  • 476
  • 699
  • 1
    This is very similar to http://stackoverflow.com/questions/12977894/what-is-the-angularjs-way-to-databind-many-inputs, but the 2nd approach is new here, so it is not exactly a duplicate. I provided a detailed answer (similar to Artem's) there, explaining how ng-repeat child scopes work. – Mark Rajcok Dec 08 '12 at 22:57
  • Since it cost me reasonable amount of googling before finally finding this thread, may I rename it to sth like: "Parent Model not updated from ngRepeat inputs" or "Model not updated when using ngRepeat" or "ngRepeat inputs not bound to (parent) model"? Maybe you have better ideas for the title ? – vucalur Oct 27 '13 at 11:55

8 Answers8

120

This seems to be a binding issue.

The advice is don't bind to primitives.

Your ngRepeat is iterating over strings inside a collection, when it should be iterating over objects. To fix your problem

<body ng-init="models = [{name:'Sam'},{name:'Harry'},{name:'Sally'}]">
    <h1>Fun with Fields and ngModel</h1>
    <p>names: {{models}}</p>
    <h3>Binding to each element directly:</h3>
    <div ng-repeat="model in models">
        Value: {{model.name}}
        <input ng-model="model.name">                         
    </div>

jsfiddle: http://jsfiddle.net/jaimem/rnw3u/5/

jaime
  • 41,961
  • 10
  • 82
  • 52
  • 3
    Thanks for the first hyperlink, as it explains the blur/loss of focus/flicker issue. I always wondered why that happened. – Mark Rajcok Dec 08 '12 at 22:49
  • 2
    thanks for that, helped alot. Still, binding to primitives should be there imo... – Daniel Gruszczyk Jul 07 '14 at 12:19
  • 1
    old post but thnx, took me some time to find the 'not changing the model inside ngRepeat' problem and it was your advice not to bind to primitives – Stefanos Chrs Aug 26 '14 at 16:01
  • Also: Don't modify the _entire list_ while typing - a trap I fell into. I was watching the collection for changes, and replacing it with an identical copy - so even though I wasn't binding to primitives, the elements were being recreated. – z0r Aug 03 '16 at 16:59
74

Using Angular latest version (1.2.1) and track by $index. This issue is fixed

http://jsfiddle.net/rnw3u/53/

<div ng-repeat="(i, name) in names track by $index">
    Value: {{name}}
    <input ng-model="names[i]">                         
</div>
Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
Thilaga
  • 1,591
  • 1
  • 13
  • 13
44

You get into a difficult situation when it is necessary to understand how scopes, ngRepeat and ngModel with NgModelController work. Also try to use 1.0.3 version. Your example will work a little differently.

You can simply use solution provided by jm-

But if you want to deal with the situation more deeply, you have to understand:

  • how AngularJS works;
  • scopes have a hierarchical structure;
  • ngRepeat creates new scope for every element;
  • ngRepeat build cache of items with additional information (hashKey); on each watch call for every new item (that is not in the cache) ngRepeat constructs new scope, DOM element, etc. More detailed description.
  • from 1.0.3 ngModelController rerenders inputs with actual model values.

How your example "Binding to each element directly" works for AngularJS 1.0.3:

  • you enter letter 'f' into input;
  • ngModelController changes model for item scope (names array is not changed) => name == 'Samf', names == ['Sam', 'Harry', 'Sally'];
  • $digest loop is started;
  • ngRepeat replaces model value from item scope ('Samf') by value from unchanged names array ('Sam');
  • ngModelController rerenders input with actual model value ('Sam').

How your example "Indexing into the array" works:

  • you enter letter 'f' into input;
  • ngModelController changes item in names array => `names == ['Samf', 'Harry', 'Sally'];
  • $digest loop is started;
  • ngRepeat can't find 'Samf' in cache;
  • ngRepeat creates new scope, adds new div element with new input (that is why the input field loses focus - old div with old input is replaced by new div with new input);
  • new values for new DOM elements are rendered.

Also, you can try to use AngularJS Batarang and see how changes $id of the scope of div with input in which you enter.

Community
  • 1
  • 1
Artem Andreev
  • 19,942
  • 5
  • 43
  • 42
  • thanks for the 1.0.3 explanation. It is interesting that in 1.0.3, the "bind directly" case, the input field appears to be broken, in the sense that it doesn't appear to accept any changes/typed input (at least in Chrome). I'm sure we'll see quite a few SO posts about this in the near future :) I suppose this new way is better, as it will be more obvious that something is wrong. – Mark Rajcok Dec 08 '12 at 23:10
6

If you don't need the model to update with every key-stroke, just bind to name and then update the array item on blur event:

<div ng-repeat="name in names">
    Value: {{name}}
    <input ng-model="name" ng-blur="names[$index] = name" />
</div>
Bill Heitstuman
  • 979
  • 1
  • 8
  • 23
4

I just updated AngularJs to 1.1.2 and have no problem with it. I guess this bug was fixed.

http://ci.angularjs.org/job/angular.js-pete/57/artifact/build/angular.js

Anton Rodin
  • 991
  • 2
  • 12
  • 19
2

The problem seems to be in the way how ng-model works with input and overwrites the name object, making it lost for ng-repeat.

As a workaround, one can use the following code:

<div ng-repeat="name in names">
    Value: {{name}}
    <input ng-model="names[$index]">                         
</div>

Hope it helps

Draško Kokić
  • 1,280
  • 1
  • 19
  • 34
1

I tried the solution above for my problem at it worked like a charm. Thanks!

http://jsfiddle.net/leighboone/wn9Ym/7/

Here is my version of that:

var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
    $scope.models = [{
        name: 'Device1',
        checked: true
    }, {
        name: 'Device1',
        checked: true
    }, {
        name: 'Device1',
        checked: true
    }];

}

and my HTML

<div ng-app="myApp">
    <div ng-controller="MyCtrl">
         <h1>Fun with Fields and ngModel</h1>
        <p>names: {{models}}</p>
        <table class="table table-striped">
            <thead>
                <tr>
                    <th></th>
                    <th>Feature 1</td>
                    <th>Feature 2</th>
                    <th>Feature 3</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Device</td>
                   <td ng-repeat="modelCheck in models" class=""> <span>
                                    {{modelCheck.checked}}
                                </span>

                    </td>
                </tr>
                <tr>
                    <td>
                        <label class="control-label">Which devices?</label>
                    </td>
                    <td ng-repeat="model in models">{{model.name}}
                        <input type="checkbox" class="checkbox inline" ng-model="model.checked" />
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
-5

how do something like:

<select ng-model="myModel($index+1)">

And in my inspector element be:

<select ng-model="myModel1">
...
<select ng-model="myModel2">