116
votes

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>

8
This is very similar to stackoverflow.com/questions/12977894/…, 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
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

8 Answers

120
votes

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/

72
votes

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>
43
votes

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.

6
votes

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>
4
votes

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

2
votes

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

1
votes

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
votes

how do something like:

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

And in my inspector element be:

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