Difficulty with ng-model, ng-repeat, and inputs
Asked Answered
V

8

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>

Valois answered 5/12, 2012 at 1:23 Comment(2)
This is very similar to #12978394, 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.Rissa
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 ?Enstatite
C
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/

Carpathoukraine answered 5/12, 2012 at 2:25 Comment(4)
Thanks for the first hyperlink, as it explains the blur/loss of focus/flicker issue. I always wondered why that happened.Rissa
thanks for that, helped alot. Still, binding to primitives should be there imo...Supernormal
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 primitivesMccarty
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.Scintillation
H
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>
Helicograph answered 6/2, 2014 at 12:52 Comment(3)
did the job; no more reference lossChipman
I did search so long for that.... so easy... just so hidden. Give this as the answer!Northwestwards
oooooh, so adding "track by $index" in ng-repeat also fixes the "flickering" issueWoodcock
R
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.

Resound answered 5/12, 2012 at 13:9 Comment(1)
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.Rissa
P
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>
Packard answered 9/8, 2014 at 22:6 Comment(1)
Why not using ng-model="names[$index]" ... I know it's a workaround, but it works ;-)Austriahungary
T
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

Tentacle answered 21/1, 2013 at 13:29 Comment(0)
T
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

Tudor answered 10/7, 2017 at 8:17 Comment(0)
A
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>
Autry answered 5/7, 2013 at 23:5 Comment(0)
P
-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">
Pulverulent answered 2/9, 2015 at 19:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.