AngularJS: ng-if outside ng-repeat breaks ng-repeat - javascript

AngularJS Verion: 1.3.8
JSFiddle: http://jsfiddle.net/uYFE9/4/
I've been working on a small AngularJS application, and ran into a bit of a problem. I have an ng-repeat on a page, which fills in the contents of a form. The amount of items in the form is defined by a dropdown bound to a model, and populated using ng-options. Something like:
<select id="testAmount" ng-model="selectedItem" ng-options="item.name for item in items"></select>
<form role="form" name="testForm" ng-if="!complete">
<div ng-repeat="i in getNumber(selectedItem.number) track by $index">
{{$index}}
</div>
</form>
Complete is set to false in the beginning, and hitting a Next button will toggle complete and hide the form and dropdown. A Back button will then toggle complete back, and show the form again.
The problem I'm having is with the ng-if on the select (and previously, I had the form wrapped in a div with the same ng-if - same problem). The ng-repeat no longer updates when the select dropdown is changed. Removing the ng-if on the select restores the ng-repeat to working order.
I'm wondering if there's something strange I'm doing with the nesting here, or if it's actually a bug? You can test it out on the JSFiddle linked above. The $index should be printed the number of times on the dropdown, but isn't.
Interestingly enough - when debugging the problem on my local machine, having FireBug open fixed the issue.

This is because of ng-if creating a child scope and how prototypical inheritance works with primitives. In this case, the primitive is selectedItem that you are setting by the <select>, but is actually being set on the child scope and shadows/hides the parent scope property.
In general you should always use a dot (.) with ng-models:
$scope.selection = {selectedItem: undefined};
And in the View:
<div ng-if="!complete">
<select ng-model="selection.selectedItem"
ng-options="item.name for item in items"></select>
</div>

ng-if is causing you some scoping issues (which messes with the binding).
Here is an updated jsfiddle that you could use as a work around. Essentially, this example wraps another div around the items that you want to end up hiding. And then adds a next function so that the same scope is affected during the click that sets complete to true.
HTML:
<div ng-app="test">
<div ng-controller="TestCtrl">
<div ng-if="!complete">
<div>
<label for="testAmount">Amount:</label>
<select id="testAmount" ng-model="selectedItem" ng-options="item.name for item in items"></select>
</div>
<form role="form" name="testForm">
<div ng-repeat="i in getNumber(selectedItem.number) track by $index">
{{$index + 'hi'}}
</div>
<button class="btn btn-default" value="Next" title="Next" ng-click="next()">Next</button>
</form>
</div>
<div ng-if="complete">
</div>
</div>
</div>
JS:
angular.module('test', [])
.controller('TestCtrl', function($scope) {
$scope.complete = false;
$scope.items = [
{ name: '2', number: 2 },
{ name: '3', number: 3 },
{ name: '4', number: 4 }
];
$scope.selectedItem = $scope.items[0];
$scope.getNumber = function (number) {
return new Array(number);
};
$scope.next = function() {
$scope.complete = true;
};
})

I believe the problem is with your select statement inside an ng-if the selectedItem is never getting set. If you just don't want to show that dropdown when !complete change it to an ng-show and it works fine.
<div ng-show="!complete">
As to WHY the ng-model is not being bound inside the ng-if, I don't really know but it does make some sense in that you are trying to do a conditional bind which is a bit screwy

Related

How to load controller as per specific condition using Angular.js

I am facing some issue. I have some nested controller within one parent controller and I need it to execute as per some condition using Angular.js. I am explaining my code below.
NABH.html:
<div ng-controller=NABHParentController>
<div ng-show="showNabh">
<div ng-include="'nabh1.html'"></div>
</div>
<div ng-show="showNabh1">
<div ng-include="'nabh2.html'"></div>
</div>
</div>
nabh1.html:
<div class="right_panel" style="display:block;" id="auditnabh" ng-controller="NABHController">
<td class="sticky-cell" ng-click="initiateNABH(nabh.NABHAuditID)">
</td>
</div>
nabh2.html:
<div class="right_panel" ng-controller="NABH2Controller">
<h2 class="page-title">NABH (INT012017001)</h2>
<div>
NABHParentController.js:
var app=angular.module('demo');
app.controller('NABHParentController',function($scope,$http,$state,$window,$location,$filter){
$scope.showNabh=true;
$scope.showNabh1=false;
})
NABHController.js:
var app=angular.module('demo');
app.controller('NABHController',function($scope,$http,$state,$window,$location,$filter,getBothIdToAuditDetailPage)
{
$scope.initiateNABH = function(aid) {
$scope.$parent.$parent.showNabh=false;
$scope.$parent.$parent.showNabh1=true;
}
})
Here Initially all controller are loading and nabh1.html is displaying first. When user will click on that td click event the second part html is showing. Here I need when user will click on that ng-click="initiateNABH(nabh.NABHAuditID)" the second view will open and the resepective controller will start execute. Initially only displaying view related controller will execute. Please help.
It sounds like using ng-if instead of ng-show will solve your problem:
<div ng-if="showNabh">
<div ng-include="'nabh1.html'"></div>
</div>
<div ng-if="showNabh1">
<div ng-include="'nabh2.html'"></div>
</div>
The difference is that while ng-show will "only" hide the element using css when the expression is falsy, ng-if will not create the element if it's falsy and as a result will not initiate the controller until ng-if is truthy.
Also, I would probably move the initiateNABH function to the parent controller - it will still be available in the child controller but makes the code less likely to break since you don't have to use $parent:
var app=angular.module('demo');
app.controller('NABHParentController',function($scope,$http,$state,$window,$location,$filter){
$scope.showNabh=true;
$scope.showNabh1=false;
$scope.initiateNABH = function(aid) {
$scope.showNabh=false;
$scope.showNabh1=true;
}
})

ngModel cannot detect array changes correctly

The component model:
private SomeArray = [{ key: "Initial" }];
User can add/remove items dynamically:
addField() {
this.SomeArray.push({ key: Math.random().toString() });
}
removeField(index: number) {
this.SomeArray.splice(index, 1);
}
Template markup:
<div class="col-xs-12">
<button (click)="addField()" type="button">Add</button>
</div>
<div *ngFor="let field of SomeArray; let i = index;">
<input [(ngModel)]="field.key" #modelField="ngModel" [name]=" 'SomeArray['+i+'].key' " type="text" class="form-control" required />
<div [hidden]="modelField.pristine || !(modelField.errors && modelField.errors.required)" class="alert alert-danger">
Required error
</div>
<button (click)="removeField(i)" class="btn btn-danger">Remove</button>
</div>
This works untill user removes any item from SomeArray. If I add some two items initially:
and remove the one with 1 index:
then after adding another item Angular treat it as item has both 0 and 1 index (the new item "occupies" both two inputs):
(item with key 0.1345... is not displayed)
It's worth to noting items of SomeArray are as expected, but data binding fails. What can be the reason of it?
Update: Thanks to the comments of #Stefan Svrkota and #AJT_82 it's known for me the issue can be resolved by adding [ngModelOptions]="{standalone: true}" to the needed input. But I couldn't stop thinking about the reason of the issue in my cause, without setting standalone option (there is unique value for each name attribute so it's excepted nothing wrong here).
Finally I have found that behavior occurs when input elements are into <form> tag only - Plunker sample here (enclosing of template with form tag is the reason that issue).
Any ideas of this behavior?
The reason why it happens is ngFor mixes name properties when you delete some item.
When you use ngModel inside form each ngModel control will be added to form controls collection.
Let's see what happens if we have added three items and clicked on Remove the second
1) Step1 - SomeArray[1].key exists in collection controls
2) Step2 - SomeArray[1].key has been removed from controls collection
3) Step3 - Html looks like
4) Step4 We are adding a new item
So formGroup returns existing item.
How we can solve it?
1) Don't wrap our controls in form tag
2) Add ngNoForm attribute to form
<form ngNoForm>
3) Use
[ngModelOptions]="{standalone: true}
With all three solutions above:
We can remove [name] property binding
We can't use the built in Form group validation
4) Use trackBy for ngFor
template.html
<div *ngFor="let field of SomeArray; let i = index; trackBy: trackByFn">
component.ts
trackByFn(i: number) {
return i;
}
Plunker Example
This way our built in form will work properly

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

Editing and saving is not working

I have created an application in AngularJS with edit, save and cancel options, but the problem is that when I click the edit I am not getting the value for editing and saving.
The textfield and dropdowns are been provided through ng-transclude
Can anyone please tell me some solution for this
DEMO
HTML
<div ng-controller="LocationFormCtrl">
<h2>Editors</h2>
<span ng-repeat="location in location">
<div class="field">
<strong>State:</strong>
<div click-to-edit="location.state"><input ng-model="view.editableValue"/></div>
</div>
<div class="field">
<strong>City:</strong>
<div click-to-edit="location.city"><select ng-model="view.editableValue" ng-options="loc.city for loc in location"></select></div>
</div>
<div class="field">
<strong>Neighbourhood:</strong>
<div click-to-edit="location.neighbourhood"><input ng-model="view.editableValue"/></div>
</div>
<h2>Values</h2>
<p><strong>State:</strong> {{location.state}}</p>
<p><strong>City:</strong> {{location.city}}</p>
<p><strong>Neighbourhood:</strong> {{location.neighbourhood}}</p>
<hr>
</span>
</div>
Don't really know why, I was just playing around with the code, but seems working, at least with the text fields, using ng-if instead of ng-show/ng-hide: http://jsfiddle.net/T6rA9/1/
I'll update my answer if I find a reason...
Update: I think this is what you're looking for: http://jsfiddle.net/T6rA9/7/
The difference is that instead of saving the value on save, I am reverting the changes on cancel, which is easier due to angular two-way data-binding.
Because of that, I also removed the view.editableValue ng-model directive and used the fields as you would normally do.
Transclusion and isolated scopes does not work the way you may think. You can read more about it here http://angular-tips.com/blog/2014/03/transclusion-and-scopes/
If you i.e. make this change you will already see a difference
<div click-to-edit="location.state"><input ng-model="location.state"/></div>
What about creating ngClick function which add input element inside your div with previous value?
<div class="newInput" ng-show="hidden">
<label> {{ inputValue }} </label>
</div>
<div class="newInput" ng-show="!hidden">
<input ng-model="inputValue" />
</div>
And main.js file:
app.controller('MyCtrl', function($scope) {
$scope.hidden = true;
$scope.inputValue = 'Edit me!';
$scope.addInput = function() {
$scope.hidden = !$scope.hidden;
}
});
Here you have Plunker

How can I access to the ng-repeat scope?

My ng-repeat creates different elements and I need to do some operations, like creating variables, only on the ng-repeat scope.
How can I retrieve the ng-repeat scope?
How can I do something like this?
<div ng-repeat="item in items">
<button onclick="load(item, $scope)"></button>
</div>
$scope has to be the ng-repeat scope only. Is this possible?
EDIT
that's the issue:
The code:
HTML
<div ng-repeat="item in items" class="container">
<div class="row">
Name: {{item.name}}
<select ng-model="item['id']"
ng-options="opt for opt in ids"
ng-change="loadTypes(item)"
>
<option value="">Default</option>
</select>
<select ng-model="item['type']"
ng-options="opt for opt in types"
>
<option value="">Default</option>
</select>
</div>
</div>
Controller
//Executed one time when module is loaded
$scope.init = function(){
//All ids on the DB
$scope.ids = getIdsFromServer();
}
//Executed when an Ids select changes
$scope.loadTypes = function(item){
//Possible types for an Id
types = getTypesFromServer(item.id);
/*
thisItemNgRepeatScope.types = getTypesFromServer(item.id) ?!??!! Something like this?
*/
}
The problem:
Ids it's the same in all the whole document and that's ok, it's works properly.
But I want to change the model for the second select (types) only on the ng-repeat scope. So when I change Id, I get all possible types for this Id, but only in the row where I am, where the select has been changed.
How can I do this?
the mighty 'this' in your load(item) function is the scope.
I assume that with "ng-repeat scope" are are refering to the parent scope of each individual ng-repeat loop. This can be done easily be referencing the parent scope of the current scope like this:
$scope.$parent

Categories