Angular NgModelController behaviour inside ng-repeat - javascript

Once I realized how ng-model directive works and was absolutely confident about it's behaviour this example just blowed my mind
<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>
ng-repeat fiddle. Main problem is angular's behaviour for three different versions. I understand that ng-repeat creates a new scope for each array item, I suppose (not sure) it tracks only array reference and it's size (so, array items shouldn't cause $digest loop on change), but, what we've got here (watch just first example without $index using): for version 1.0.3 < we have expected behaviour: changing 'name' property with input will only change ngModelController's value (scope inheritance) and this is fair, 1.1.1 - well, what's going on there: we don't even get new values inside of input and! we don't rerender our items cause we don't lose the focus (and i really don't understand why $digest loop fires value replacement for this input as Artem has said), third version - 1.2.1: changing input values also changes 'name' value in the outer scope (as I understand ngModelController should inherit a scope created by ng-repeat directive). So, what really happens (and why) in all three examples?

Using Angular latest version (1.2.1) and track by $index. This issue is fixed
http://jsfiddle.net/rnw3u/55/
<div ng-repeat="name in names track by $index">
Value: {{names[$index]}}
<input ng-model="names[$index]">
</div>

Related

Angular Drop-down inside of ng-repeat with additional logic needed for selected option

This is basically a 2 part question which is why some of the other answers I've found on StackOverflow don't sufficiently cover what I'm asking.
Also need to note: using Bootstrap.
My simplified html can be explained like this:
<div class="container-fluid">
<div class="row"> Headers </div>
<div class="ParentObjectDiv" ng-repeat="pobj in pobjlist">//pobj list is returned from call in controller
<div class="row"> ParentObject values </row>
<div class="ChildObjectContainer">
<div class="row"> Headers </div>
<div class="ChildObjectDiv" ng-repeat="cobj in pobj.cobjs">
<div class="row">
<div class="col-md-1">
<select ng-model="cobj.someproperty"
ng-options="option for option in optionlist">
//Here is the issue : the optionlist is returned from a separate call in the constructor
//The cobj.someproperty can be null, and when that's the case, I want a custom selection
//that says please select a property
</select>
</div>
</div>
</div>
</div>
</div>
</div>
Ok, so on to what I have tried.
Can't do a selectedOption = "whatever" in the controller for obvious reasons.
I have tried adding an angular expression that sets the property of cobj.someproperty to a value like "Value not know" if it detects that the property is empty/null.
This is probably coming down to a fundamental misunderstanding of angular on my part. I have tried putting the expression that assigns this "not known" value to the property in various expression directives and just in the html itself. It is not an option to have this "not known" value on the actual object in the database and the list of Pobj's and their Cobj's is very large so I don't think I can spare iterating the entire list structure to set it before rendering and then iterating it again in the ng-repeats. I'm hoping that I can have a quick expression that evaluates null and sets it. Something like:
{{cobj.someproperty = cobj.someproperty ? cobj.someproperty : "value not known"}}
But not sure if that's a valid expression and if it is where I would put it because so far I've tried putting it in the ng-model itself, an ng-if, and just a blank line of html all within the scope of the cobj in the ng-repeat and none are working.
I haven't "tried" this, but my preferred solution and the one I've been researching is to see if there is a way to instead of evaluating and changing the cobj.someproperty, to change the ng-model of the select to point at a dummy property and at the same time set the cobj.dummyproperty to cobj.someproperty if some property is set, otherwise to set it to "value not known".
Additional considerations :
The parentobject list can be very large (>1k) and has on average 3 childobjects (but can have anywhere from 1 to 20) so performance is an issue.
There will be many cases where the user will not want to set this property so "value not known" when null will not be a temporary thing.
A user also needs to be able to select "value not known" if they have later realize they were mistake about the cobj.someproperty.
I am going to work on the logic to push the changes back to the database later so I am not worried about how the value is stored in the model in angular because I will most likely not use the ng-model to update the field. Right now I'm just worried with the selected option of the drop down.
Here is the answer with simplified html.
JavaScript code
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.items = [{
"someProperty": "abc"
}, {
"someProperty": "xyz"
}, {
"someProperty": null
}]
$scope.optionlist = ["abc","xyz","pqr"];
});
HTML code:
<table ng-controller="MainCtrl" >
<tbody>
<tr ng-repeat="item in items track by $index">
<td width="75%">
{{ item.someProperty }}
</td>
<td width="25%">
<select ng-model="item.someProperty" >
<option value="">Value not know</option>
<option value="{{option}}" ng-repeat="option in optionlist">{{option}}</option>
</select>
</td>
</tr>
</tbody>
So whenever angular finds a null value for someProperty it binds the value to "Value Not Know". I am using ngRepeat instead of ngOption. Let me know if this is what you are looking for.
here is my plunk

Angularjs ng-repeat index not working

I want to use ng-repeat more or less as follows:
<div ng-repeat="One_Entry in Entries_List track by One_Entry.Entry_ID"
onClick="DoSomething(One_Entry.Entry_ID)">
<!---
present various fields within "One_Entry"
-->
</div>
Entries_List is a JSON array of objects, being Entry_ID one of the elements within the object.
DoSomething is a function within the related controller that is supposed to perform a specific activity on the structure whose ID is the passed Entry_ID.
I tried using $index as well as $parent.$index but I'm getting an error stating that these variable do not exist.
Could anyone tell me how I can achieve the above functionality?
Thanks.
The object One_Entry is scoped. Therefore onclick won't work. Use ng-click instead which is the Angular version for onclick:
<div ng-repeat="One_Entry in Entries_List track by One_Entry.Entry_ID"
ng-click="DoSomething(One_Entry.Entry_ID)">
<!---
present various fields within "One_Entry"
-->
</div>
It's not OnClick it's ng-click
Change
From :
<div ng-repeat="One_Entry in Entries_List track by One_Entry.Entry_ID"
onClick="DoSomething(One_Entry.Entry_ID)">
To:
<div ng-repeat="One_Entry in Entries_List track by One_Entry.Entry_ID"
ng-click="DoSomething(One_Entry.Entry_ID)">

ng-model binding not updates another ng-model on the same object

I have this code
http://plnkr.co/edit/aycnNVoD96UMbsC7rFmg?p=preview
<div data-ng-app="" data-ng-init="names=['One']">
<input type="text" ng-model="names[0]">
<p>Looping with ng-repeat:</p>
<ul>
<li data-ng-repeat="name in names">
<input type="text" ng-model="name"> {{ name }}
</li>
</ul>
</div>
When i change value of name[0] in the first input box it changes values of the second input box.
But when i change value of name[0] in the second input box it does not change value of the first input box. Why?
It works if you bind your second input to : names[$index]
<input type="text" ng-model="names[$index]"> {{ name }}
This is due to ng-repeat creating a child scope, so the reference to name inside the ng-repeat is different to that original one in the names array, see here:
New AngularJS developers often do not realize that ng-repeat,
ng-switch, ng-view and ng-include all create new child scopes, so the
problem often shows up when these directives are involved. (See this
example for a quick illustration of the problem.)
Regarding as to why this happens, when you bind the input to name in names inside the ng-repeat, you are creating a new property on the new child scope created by the ng-repeat called name, and thus the ng-model of the textbox created by the ng-repeat is referencing a different name to that of the actual 0th element of the names array. As others have pointed out, if you use names[$index] you are implicitly referencing the 0th element of the names array, thus NOT creating a new name property on the child scope created by the ng-repeat. An angular best practice is not to have ng-models bound to primitives, rather objects, Sandy has mentioned in his answer if you bind to an object you will overcome this, and the 2 other posters have answered this by using $index to refer to the 0th element of the names array. This is one of the nucances of scope inheritance in angular.
A couple more handy links:
Here and here.
Just wanted to give my bit on this. Somewhat related to your problem as I see.
<body>
<div data-ng-app="" data-ng-init="names=[{value:'One'}, {value:'Two'}]">
<p>Looping with ng-repeat:</p>
<ul>
<li data-ng-repeat="name in names">
<input type="text" ng-model="name.value"> {{ name }}
</li>
</ul>
</div>
</body>
Instead of binding the array item directly to the control, I would prefer to create an object of the array and then bind value of each item. This way we can avoid reference problems.
A working prototype jsfiddle
Hope it helps.
You need to provide $index in your ng-model.
<li data-ng-repeat="name in names">
<input type="text" ng-model="names[$index]"> {{ name }}
</li>
You are binding ng-model="names[0]". So it means that you are binding value on first index of names array.
So when we write ng-model="names[$index]" in ng-repeat it means that all values will be bound accordingly into array. $index is an iterator offset of the repeated element.
names[0] = 'One'
names[1] = 'Two'
and so on!

AngularJS Nested ng-repeat Not Working in Version 1.2.1

I am working on a system where I would like to nest two ng-repeat statements so that I can work my way through a 2D array. I was able to complete the task successfully using version 1.1.1 as you can see here:
http://jsfiddle.net/skArT/1/
However, when I take the exact same code set and change the version of Angular to 1.2.1 the code no longer works and throws an error:
http://jsfiddle.net/skArT/2/
Error:
Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: x in value, Duplicate key: number:0
So my question is, how can I accomplish the task shown in version 1.1.1 with newer versions of Angular?
All you need to do is read the error message and listen to it. Change:
<div ng-repeat="x in value">{{x}}</div>
To:
<div ng-repeat="x in value track by $index">{{x}}</div>
Adding a track by clause solves this. It seems rather trivial to me for it to be required in your case.
<body ng-app="myApp" ng-controller="myCtrl">
<span ng-repeat="(index, value) in field">
<div ng-repeat="(key,x) in value track by key">{{x}}</div>
<br/>
</span>
</body>
http://jsfiddle.net/92bSt/

Bind ng-models in input type checkbox

I have a problem when binding ng-models with ng-repeat in a input tag type checkbox.
I will first attach my code and then explain more in detail.
app/main.html:
<div ng-repeat="feature in features">
<input type="checkbox" ng-model="features[$index].name">{{features[$index].name}}
</div>
<br></br>
<div class="highlighter">
<span ng-class="{emo:Emotions}">Manually</span> <span ng-class="{feel:Feelings}">create</span> the <span ng-class="{emo:Emotions}">entire</span>
</div>
main.js
angular.module('webClientApp')
.controller('MainCtrl', function ($scope,$http) {
[...other variables...]
$scope.features = [{'name':'Emotions'},{'name':'Feelings'}];
[...other parts of code]
});
Let's also assume that in the main.css file there are references to the classes .emo' and.feel' respectively to highlight the target word when the user ticks the box relative to the feature.
Now, the application works correctly when I listed all the inputs one by one like the following:
<input type="checkbox" ng-model="Emotions">Emotions
<input type="checkbox" ng-model="Feelings">Feelings
but I wanted to wrap it into an ng-repeat and list the features in the controller scope, since the features I will considered will be more. When I try the code above when I tick on the box the name changes to `true'.
I have read a lot about how to bind models to an ng-repeat inside a input tag but none of the solutions apply to my case.
Can someone please help me?
I changed thigs up quite a bit from your original model but... I did get something to behave similar to what you are looking for.
HTML
<div ng-app="webClientApp">
<div ng-controller="MainCtrl">
<div ng-repeat="(feature,enabled) in features">
<input type="checkbox" ng-model="features[feature]">{{feature}}</input>
</div>
<div class="highlighter">
<span ng-class="{emo:features.Emotions}">Manually</span> <span ng-class="{feel:features.Feelings}">create</span> the <span ng-class="{emo:features.Emotions}">entire</span>
</div>
{{features}}<br>
{{features.Emotions}}<br>
{{features.Feelings}}
</div>
JS
var app = angular.module('webClientApp', []);
app.controller('MainCtrl', function($scope, $http) {
$scope.features = {Emotions: true, Feelings: true};
});
Here's the fiddle: http://jsfiddle.net/rodhartzell/8YrxQ/
Hope this helps.
(i should add this as a comment, but I don't have enough rep. yet)
There is an issue on github which concerns your issue: https://github.com/angular/angular.js/issues/1404 and the comment of caitp shows some workarounds: https://github.com/angular/angular.js/issues/1404#issuecomment-30859987
You could (also) define a new javascript object in your controller and map the elements to that.
In controller: $scope.awnsers = {};
In template: ng-model="awnsers[feature.name]"
I hope this helps
You must use ng-checked instead of ng-model.
Check out this jsfiddle
http://jsfiddle.net/fizerkhan/z5z9s/24/
ngModel and ngChecked are not meant to be used together.
ngChecked is expecting an expression, so by saying ng-checked="master". If the expression is truthy, then special attribute "checked" will be set on the element
You should be able to just use ngModel, tied to a boolean property on your model. If you want something else, then you either need to use ngTrueValue and ngFalseValue (which only support strings right now), or write your own directive.

Categories