ng-repeat one-time binding inheritance? - angularjs

If I have an ng-repeat directive iterating through a list of lists via a one-time binding, does a nested ng-repeat through each of the inner lists also need a one-time binding, or is that redundant?
<div ng-repeat="list in ::$ctrl.lists">
<span ng-repeat="item in list">{{item}}</span>
</div>
Do I need to change the inner ng-repeat to item in ::list? $ctrl.lists will never change in any manner.

It is specify nowhere that is is inherit, event if you have bind the array, object inside the array can still change, so it is better to put '::' for the span loop too.
http://jsfiddle.net/vw2fjxys/64/
var a = [1,2,3]
setTimeout(function(){
console.log(2)
a.length = 2
$scope.$apply();
},3000)
$scope.lists = [a];
You can see on the exemple after 2 second that with :: it is not recalculated for the inner loop but without it is.

Related

Use ng-repeat's $index variable within an AngularJs expression

I have an ng-repeat div that I need to use the $index variable to build out an expression. Consider the following:
<div ng-repeat="item in myItems track by $index">
This will throw an error: {{myItemId_{{$index}}.someProperty}}
</div>
If I have a $scope variable called "myItemId_0", how do I use $index within a curly braces {{}} expression? I have tried omitting the curly braces for the $index variable, this but this doesn't work either:
{{myItemId_$index.someProperty}}
Since it's a variable on scope, create a function that retrieves your property via bracket notation.
HTML:
{{getByIndex($index, "someProperty")}}
Controller:
$scope.getByIndex = function(index, someProperty) {
var propertyName = "myItemId_" + index;
return $scope[propertyName][someProperty];
}
However, the logical solution is to not access your properties in this way at all, but rather use item which is already available from your ng-repeat.
HTML:
div ng-repeat="item in betterItems track by $index">
{{item.someProperty}}
</div>
Controller:
$scope.betterItems = [];
angular.forEach(myItems, function (i) {
$scope.betterItems.push(createMappedItem(i));
}
Even if you have to map your collection to a new collection (or reuse the same if need be), you can still access that original item. It's cleaner and much easier to maintain. In addition, retrieval by index can be very dangerous if you start mutating the collection.

NG-Repeat doesn't update cleanly when scope is updated

So I have a list of workspaces:
<li class="workspace-object" ng-repeat="w in workspaces | filter:searchQuery" ng-click="selectWorkspace(w)">
{{w.name | titleCase }}
</li>
That List gets updated in the database by some function, and then I call the loadWorkspaces function below, to re-populate the $scope.workspaces object
$scope.loadWorkspaces = function() {
APIService.getWorkspaces().then(function(data){
$scope.workspaces = data;
});
}
When this happens, the $scope.workspaces, when logged out, reads the right updated information immediately, however, the ng-repeat DUPLICATES, before it updates to the proper list. I have no idea why this happens. Any ideas?
In this example, I am updating the workspace title, and that update runs the loadworkspaces function.
Try this ...
w in workspaces track by $index
When the contents of the collection change, ngRepeat makes the corresponding > changes to the DOM:
When an item is added, a new instance of the template is added to the DOM.
When an item is removed, its template instance is removed from the DOM.
When items are reordered, their respective templates are reordered in the DOM.
By default, ngRepeat does not allow duplicate items in arrays. This is because when there are duplicates, it is not possible to maintain a one-to-one mapping between collection items and DOM elements.
If you do need to repeat duplicate items, you can substitute the default tracking behavior with your own using the track by expression.
For example, you may track items by the index of each item in the collection, using the special scope property $index
Accepted answer did not help me (Using Angular 1.1.10), my repeat was still updating delayed. I struggled for ages before I got it working.
Instead of repeating my custom directive like so;
<my-directive ng-repeat="item in myarray track by item.id" some-binding="item"></my-directive>
I (magically) got it working after moving the ng-repeat to a (non-directive) parent element, like so:
<div ng-repeat="item in myarray track by item.id">
<my-directive some-binding="item"></my-directive>
</div>

AngularJS - original index of an object within filtered ng-repeat

I am using a nested ng-repeat and a filter on a object. The first ng-repeat is filters to the headerId in a gapHeader object. The second ng-repeat filters gapSection, sectionId to the corresponding headerID.
I have an edit page which is within a separate modal window. The purpose is to edit content corresponding to the headerID & sectionID of the sub-object) This also has a separate control. Data is shared through a service.
My problem I have a button for each gapSection sub-object, which opens the edit page modal, when I pass the $index value for the current section within each section to the service, I get the $index only corresponding to the second ng-repeat? For example, if I click the button within the 2 ng-repeat on gapSection (headerId:2, sectionId:2), I get an $index of 1. I require an $index of 2 which corresponds the sub-object position within gapSection.
Is it possible to pass the true $index which corresponds to the $index defined in the original un-filtered object of gapSection? Appreciate any comments on this and thank you!
Object:
var data ={
gapHeader:[{headerId:1,name:"General Requiremets",isApplicable:true},
{headerId:2,name:"Insurance",isApplicable:false}],
gapSection:[{headerId:1,sectionId:1,requirement:"A facility is required to have company structure",finding:null,cmeasures:null,cprocedures:null,personResp:null,isAction:null},
{headerId:2,sectionId:1,requirement:"Organisation must have public liablity",finding:null,cmeasures:null,cprocedures:null,personResp:null,isAction:null},
{headerId:2,sectionId:2,requirement:"Facility must hold workers compensation insurance",finding:null,cmeasures:null,cprocedures:null,personResp:null,isAction:null}]
};
If you need the true index you do not even need to pass the $index property, just pass the object and get the index from the original list.
i.e
$scope.gapSectionFn = function(obj){
var idx = data.gapSection.indexOf(obj);
}
Also it is not clear your issue could really be a nested ng-repeat issue, because according to you gapSection is the inner ng-repeat and you are invoking the call from inner ng-repeat and in need of gapSection's index. It should just be available, but the presence of a DOM filter will just reorg the items and its index which you can also get by doing an ng-init, i.e on the view ng-init="actIndex=$index" and use actIndex.
If you are trying to access parent ng-repeat's index then, ng-init is more appropriate than $parent.$index. Since ng-init is specially designed for that., on the parent ng-repeat you would write ng-init=""parentIndex=$index" and use parentIndex.

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

Why does the ng-repeat directive create a child scope?

Question: Why does the ng-repeat directive create a child scope?
I don't understand why this is the case. Creating a new scope makes sense to an extent if you explicitly create a child controller, but why would it automatically create one for every ng-repeat?
I guess the reason it confuses me is because if you create a loop in JS, that doesn't mean the code outside the loop can't access any of the variables inside of it.
Example:
for(var x=0; x<10; x++) {
var y = x
}
alert(y);
From the docs (with my emphasis):
The ngRepeat directive instantiates a template once per item from a
collection. Each template instance gets its own scope, where the given
loop variable is set to the current collection item, and $index is set
to the item index or key.
Seems pretty straightforward, doesn't it?
The DOM is not javascript. The items in the collection you are ng-repeating over need to be bound to separate scopes that are bound to the repeated DOM elements and creates a tree of scopes that mirror structure of the page (somewhat)
ex, this markup:
<body ng-app="myapp">
<div ng-repeat="item in items">
<p>{{ item.name }}</p>
</div>
</body>
will produce something like the DOM on the left and scope on the right:
body rootScope
div scope
p
div scope
p
(somewhat approximate for illustration)
Javascript was written by a single person in 10 days. In almost every other language, loops create a separate scope and your code example wouldn't compile or you'd end up with null/none for y in your last line.

Resources