42

Here is a demo to my problem.

$scope.myNumbers = [10, 20, 30];

<div ng-repeat="num in myNumbers">
    <input type="text" ng-model="num">
    <div>current scope: {{num}}</div>
</div>

Can anyone explain to me why are the inputs uneditable/readonly? If it's by design, what's the rationale behind?

UPDATE 2/20/2014

It looks like this is no longer an issue for v1.2.0+ Demo. But do keep in mind that although the user controls are now editable with the newer angularJS versions, it is the num property in the child scopes, not the parent scope, that get modified. In another words, modifying the values in the user controls does not affect the myNumbers array.

tamakisquare
  • 16,659
  • 26
  • 88
  • 129
  • Relevant AngularJS issue: https://github.com/angular/angular.js/issues/1267 – caw Sep 17 '14 at 00:32
  • For anyone else looking for an answer, excepted one here is no longer valid. See one by sebnukem. – FDIM Oct 21 '14 at 13:13
  • @FDIM - I am not sure if I understand what you are saying. The accepted answer (from Mark Rajcok) is still a valid solution, even for the newest Angular version (1.3). The approach of using `$index` is simply another working solution but it does not make the accepted answer invalid. – tamakisquare Oct 21 '14 at 17:56
  • True, but its unnecessary complexity if version 1.3 used. I should have written that there is a better way, bad choice of words, sorry. – FDIM Oct 22 '14 at 06:49

5 Answers5

67

Can anyone explain to me why are the inputs uneditable/readonly? If it's by design, what's the rationale behind?

It is by design, as of Angular 1.0.3. Artem has a very good explanation of how 1.0.3+ works when you "bind to each ng-repeat item directly" – i.e.,

<div ng-repeat="num in myNumbers">
  <input type="text" ng-model="num">

When your page initially renders, here's a picture of your scopes (I removed one of the array elements, so the picture would have fewer boxes):

enter image description here (click to enlarge)

Dashed lines show prototypical scope inheritance.
Gray lines show child → parent relationships (i.e., what $parent references).
Brown lines show $$nextSibling.
Gray boxes are primitive values. Blue boxes are arrays. Purple are objects.

Note that the SO answer of mine that you referenced in a comment was written before 1.0.3 came out. Before 1.0.3, the num values in the ngRepeat child scopes would actually change when you typed into the text boxes. (These values would not be visible in the parent scope.) Since 1.0.3, ngRepeat now replaces the ngRepeat scope num values with the (unchanged) values from the parent/MainCtrl scope's myNumbers array during a digest cycle. This essentially makes the inputs uneditable.

The fix is to use an array of objects in your MainCtrl:

$scope.myNumbers = [ {value: 10}, {value: 20} ];

and then bind to the value property of the object in the ngRepeat:

<div ng-repeat="num in myNumbers">
  <input type="text" ng-model="num.value">
  <div>current scope: {{num.value}}</div>
Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 5
    Mark, your answer has answered my desire to understand what's happening behind-the-scene. It's the type of answer I was looking for. Also, some of your other SO answers have helped me a lot. Thank you for sharing your knowledge and passion with the AngularJS community. Cheers. – tamakisquare Mar 19 '13 at 07:08
  • So if you have an object with referencing ID's: `myObj.myRef=[8,20]`, the only way is to convert this to `[{value:1, value:2}]` right? Would you then suggest to rather store references as strings `myObj.myRef=["8","20"]`? – Dani Apr 06 '13 at 18:33
  • 1
    @user785529, yes, you need an array of objects if you want to use ng-repeat and ng-model inside. Strings are primitives also, so they don't help solve the problem. – Mark Rajcok Apr 07 '13 at 02:23
  • I tried using ng-repeat="(key,num) in myNumbers" ng-model="myNumbers[key]" I assumed this would work since it's a reference not just a value. It allows you to edit, but the input loses focus on each keystroke - any ideas? http://jsfiddle.net/ADukg/3055/ – Brad Barrow Jun 14 '13 at 02:01
  • To answer my own comment, I realise now that each ng-repeat scope is using myNumbers from the parent scope. The minute we change one of the models, the myNumbers collection on the parent scope is dirty, causing the whole ng-repeat to be re-created. Thus the input elements are now different entities and so your cursor loses focus. – Brad Barrow Jun 14 '13 at 02:14
  • Is ng-model="num.value" a good practise? How can I be able to reference back the model later? Is it just $scope.10? Please bear with me as I'm fairly new to Angular. – Roy Lee Aug 13 '13 at 06:34
  • 1
    @Roylee, using "num.value" is the recommended practice: "always have a dot in your models" -- [Miško video](http://www.youtube.com/watch?v=ZhfUv0spHCY&t=32m50s). In your controller, you would reference it as `$scope.myNumbers[0].value`. – Mark Rajcok Aug 13 '13 at 13:55
  • @MarkRajcok I added my own solution to this question below. I'm relatively new to Angular so if you had a chance to take a look at it, I would appreciate any feedback you may have. – aidan Dec 18 '13 at 05:25
  • 1
    @aidan - Mark has actually commented on the solution you mentioned in AngularJS's wiki. See it for yourself. [Link](https://github.com/angular/angular.js/wiki/Understanding-Scopes) – tamakisquare Dec 18 '13 at 18:12
34

This problem is now addressed by more recent versions of AngularJS with the track by feature allowing repeaters over primitives:

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

The page will not get repainted after each keystroke, which solves the problem of the lost focus. The official AngularJS doc is quite vague and confusing about this.

sebnukem
  • 8,143
  • 6
  • 38
  • 48
9

Seems that Angular is not able to write to model defined that way. Use reference to initial $scope attribute to let it bind value right way:

<div ng-repeat="num in myNumbers">
  <input type="text" ng-model="myNumbers[$index]">
</div>
Dmitry Evseev
  • 11,533
  • 3
  • 34
  • 48
  • 4
    Thanks Dmitry. Your solution works but not quite there. I can see the databinding is working but it's weird that the focus on the input is automatically gone after a keystroke. Take a look for yourself [here](http://embed.plnkr.co/TWTsFuxeZWWchgALdYss). – tamakisquare Mar 18 '13 at 23:05
  • Yep, it's caused by fact that angular updates view everytime you change model. – Dmitry Evseev Mar 18 '13 at 23:06
  • 3
    Avoid using `$index` for anything goes into controller. If you filter the data on the view via `ngFilter`. The index of the view would not refer to the index in the original array since the array in the view and model would be different. – Umur Kontacı Mar 18 '13 at 23:07
  • Also, I am more interested to see why binding to the variable from the repeat expression (ie. `num` in this case) doesn't work, whether it's a bug or by design. – tamakisquare Mar 18 '13 at 23:08
  • Actually, making editable collection with binded view is rather tricky task. If you are interested see please one of approaches to solve it: http://iweinfuld.posterous.com/expanding-collections-with-inline-edit-in-ang – Dmitry Evseev Mar 18 '13 at 23:12
  • @fastreload - great tip on using `$index`. I didn't know. Thanks. – tamakisquare Mar 18 '13 at 23:20
  • @DmitryEvseev - But updating view shouldn't blur the current focus on the input. Consider we have `$scope.foo = "hello"` and binding a `` and a `` to it. `` gets updated when I change `foo` via `` but the focus remains on the input all the time. – tamakisquare Mar 18 '13 at 23:33
  • @tamakisquare "The flicker is also expected with your workaround since data binding changes the array, which then makes it look like to the repeater as if value left and new value appeared, so the repeater removes the old DOM node and replaces it with a new DOM node hence the loss of focus. I know this is counter intuitive, but that is how the system is architected. Don't bind to primitives is my answer." https://github.com/angular/angular.js/issues/1267#issuecomment-7762446 – caw Sep 17 '14 at 00:32
9

ngRepeat uses a reference to the source array. Since integer (Number in js) is a value type, not a reference type, therefore cannot be passed by reference in javascript. The change will not be propagated.

Here is a demonstration:

   var x = 10;
   var ox = {value:10};

   var y = x;
   var oy = ox;

   y = 15
   oy.value = 15;

What would be the values of x and ox?

>> x = 10;
>> y = 15;
>> ox = {value:15};
>> oy = {value:15};

All javascript objects are passed by reference and all primitives are passed by value ["string", "number", etc].

Working plunker http://plnkr.co/edit/7uG2IvAdC2sAEHbdHG58

Umur Kontacı
  • 35,403
  • 8
  • 73
  • 96
  • fastreload, your explanation makes sense but I am getting comflicting information. I read that `ngRepeat` creates a new copy of the value for each of its child scopes when the item is primitive. Please go to the section on **ng-repeat** of this [SO answer](http://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs) – tamakisquare Mar 18 '13 at 23:16
  • Mark's answer is more explanatory than mine on how do scopes work. But they are not conflicting answers. In my answer, I meant that y is `copied by value` whereas oy is `copied by reference`. And he also says "If item is a primitive (as in myArrayOfPrimitives), essentially a __copy of the value__ is assigned to the new child scope property." – Umur Kontacı Mar 18 '13 at 23:28
  • That's good. I just want to make sure we are on the same page. With yours and Mark's explanation in mind, I was expecting that my inputs can be bind to the **copy of the value** and thus work with the copy. I guess AngularJS wants to make sure developers work with one single source of data so they made inputs binding to copy of the data uneditable in this case. Do you think that's the rationale behind? – tamakisquare Mar 18 '13 at 23:43
  • 1
    It's not about angularjs, it is about how javascript works. Primitives are not mutable. Therefore has to be copied. In the example with `x` and `y` if you expect them to be equal. Then also it would be `15 = 10`. If you want to read more, keywords are mutable, immutable, copy by value, copy by reference. – Umur Kontacı Mar 19 '13 at 09:36
9

I had a similar issue (and also required 'add' and 'remove' functionality), and solved the problem like so:

$scope.topics = [''];
$scope.removeTopic = function(i) {
   $scope.topics.splice(i, 1); 
}

<div ng-repeat="s in topics track by $index">
    <input ng-model="$parent.topics[$index]" type="text">
    <a ng-click="removeTopic($index)">Remove</a>
</div>

<a ng-click="topics.push('new topic')">Add</a>
aidan
  • 9,310
  • 8
  • 68
  • 82