I am trying to learn angular. I am having a hard time understanding part of this: http://www.learn-angular.org/#!/lessons/repeaters
In that example we're looping through a collection and displaying each element. Easy enough.
What I don't understand is: the collection is named "products" and each element is referred to as "product" no where in the JS code is the word product. How does angular know to call the element by "product"? Does a collection have to be plural and an element singular in name?
The identifier is assigned in the view (aka, the HTML):
<tr ng-repeat="product in products">
ng-repeat takes an expression that defines the identifier it will assign elements in the collection. So, in this case, you bind each item in products to product. This is then accessible in the scope, so any child element of the tr can access product on the scope as $scope.product.
You can name the identifier anything you want. product in products is equally as valid as bananas in apple, so long as your collection is on $scope.apple and you use the identifier bananas to refer to the item in each collection.
The first variable name passed to an ng-repeat is scoped to that ng-repeat. It's only on the template, not in the JS code, and is only available in that repeat block. So, it's essentially made up. Common conventions have people call it the singular form of the plural array (i.e. for product in products, for item in items, etc.), but that variable only refers to the current ng-repeat element on the template inside the ng-repeat.
Related
I recently got the console error `
Error: [ngRepeat:dupes] duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys...
— AngularJS Error Reference - ngRepeat:dupes
which I then used 'track by $index' and the issue was solved...
But that got me thinking... is there a reason why you wouldn't want to use track by $index in an ng-repeat?
I've read SO questions like this one as well as other articles, but it seems like almost all of the articles only talk about the advantages of using 'track by'.
Can someone list the disadvantages and give an example of when you wouldn't want to use track by $index?
It has a disadvantage,
'track by' expression tracks by index in the array. It means that as long as the index stays the same, angularjs thinks it's the same object.
So if you replace any object in the array, angularjs think it didn't change because the index in the array is still the same. Because of that the change detection wont trigger when you expect it would.
Take a look at this example
Try to change the name, nothing happens.
Remove track by index, it works.
add track by item.name, it still works.
There are multiple reasons to avoid track by $index
Avoid using track by $index when using one-time bindings
Avoid track by $index when there is a unique property identifier
Other Examples of problems with track by $index
Avoid using track by $index when using one-time bindings
The documentation specifically states that track by $index should be avoided when using one-time bindings.
From the Docs:
Avoid using track by $index when the repeated template contains one-time bindings. In such cases, the nth DOM element will always be matched with the nth item of the array, so the bindings on that element will not be updated even when the corresponding item changes, essentially causing the view to get out-of-sync with the underlying data.
— AngularJS ng-repeat Reference - Tracking and Duplicates
Avoid track by $index when there is a unique property identifier
Avoid track by $index when there is a unique property identifier to work with. When working with objects that are all unique, it is better to let ng-repeat to use its own tracking instead of overriding with track by $index.
From the Docs:
If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
— AngularJS ng-repeat Directive API Reference - Tracking
Other Examples of problems with track by $index
I have also seen problems when objects are added and removed from arrays of objects.
Problems with track by $index with Angular UI Carousel
Ng-Repeat showing irregular behavior with one time binding
Angular: Updating $scope with new data causes old data to remain when using ng-repeat
Trouble with AngularJS and Select binding not loading default value
Lets assume I am binding one big nested object to the $scope of the view shown in the code. Now, the value of an "e" object is updated. This would cause angular the check all bindings and update the DOM. If I used "track by" instead, in each ng-repeat directive, would that mean that only the binding for the "e" object would react and the dom for the "e" object be updated?
<div ng-repeat="a in b">
<div ng-repeat="c in a">
<div ng-repeat="d in c">
<div ng-repeat="e in d">
{{e.value}}<br>
</div>
</div>
</div>
</div>
The bindings will be checked no matter what, and updated only if different, per the digest cycle. As for re-building the DOM elements, Angular uses unique identifiers to determine whether each item in an ng-repeat already has a matching DOM element, or if it needs to render a new one.
By default, Angular creates and manages these unique identifiers under the hood, using the $id of each object (or $$hashKey).
track by was added later, as a way to tell Angular to use a unique identifier of your choice, rather than managing it under the hood.
This is useful when updating the data removes/changes the $id or $$hashKey, triggering unnecessary re-builds of each DOM element, even when the data didn't change at all.
Consider this example:
You have an ngRepeat which displays data records:
<li ng-repeat="item in data">{{item.value}}</li>
You use a service DataService to update your data, which has a fetch() method which retrieves data from an SQL database, and returns the records.
Updating the data in your $scope involves calling that service, and re-assigning your data variable to the result:
$scope.data = DataService.fetch();
That means, even if only one item was different, all the $id or $$hashKey properties are gone or different, and Angular will assume all items are new. It will re-build all the DOM elements from scratch.
However, since your data is from an SQL database, you already have a unique identifier (primary key), the id column. You could then change your ngRepeat to be:
<li ng-repeat="item in data track by item.id">{{item.value}}</li>
Now, instead of looking for $$hashKey, which gets lost every time you re-assign the data, Angular will use the property you told it to (item.id). Since that property does persist across re-assigning the variable, the list is once again optimized, because Angular will only re-build DOM elements for new items.
I have a tree like structure, received as JSON from some PHP code which I wrote for my server.
The code is far to complex to post, so let's use an example:
A company has
multiple departments, each of which has 0..n
jobs, each of which has 0..n
people
So, I can find myself with something like
$scope.departments[1].jobs[1].people[1]
$scope.departments[1].jobs[1].people[2]
etc
I have $scope variable for the current user-selected department, job and person.
My problem is where I want to use an ng-repeat of the jobs in the HTML view for the jobs.
The statement
<div ng-repeat="job in departments[{{departmentId}}].jobs>
gives Error: [$parse:syntax], as does
<div ng-repeat="job in departments[$scope.departmentId].jobs>
(which I tried in desperation).
What is the correct syntax?
I am wondering if I will need to try
<div ng-repeat="job in GetJobsForCurrentDepartment()>
since $scope.departmentId would be in scope in my controller, but is not in the HTML view for the departments.
Since a view has a controller associated with it, and controller has a $scope associated with it. It is not required to use $scope the html element for referencing the variables defined in the controller. Any variable used with a element in a view is looked up in the scope associated with that element. $scope is required only in the script.
Thus you can write
<div ng-repeat="job in departments[departmentId].jobs>
This will fetch the value of departmentId & jobs inside it will be processed by the ng-repeat directive.
You don't have to mention $scope attached with a varable in view
try like this
<div ng-repeat="job in departments[departmentId].jobs>
JS
$scope.departmentId=1;
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.
Given an object below -
function PersonCtrl(){
$scope.persons = [{name: "Mike", age:21,
occupation:{qualification: "engineer", company:"Intel"}}];
}
and the DOM below -
<ul>
<li ng-repeat="person in persons">
Name : {{person.name}}
<div ng-model="person.occupation">
Qualification : {{person.occupation.qualification}}
</div>
</li>
</ul>
I have a list of persons whose names have to be displayed in the list. Now I will initially load the data without any details, in this case qualification of the person.
When someone clicks on the person's name, I will retrieve the persons details. I would like to just change the model, ie add the qualification details to that person's model, and angular to then create the DOM.
One way to control this is use the ng-show, and set its expression, so that it only shows the qualification div, if and when the qualification object has values. However this will also lead to the details div being created for every person, and thus bad for performance.
Is there a way that the dom is created / destroyed by angular when an expression evaluates to true or false ?
If we want to physically remove / add parts of the DOM conditionally the family of ng-switch directives (ng-switch, ng-switch-when, ng-switch-default) will come handy.
If the detail data is small, and there's no huge cost to getting it, or rules about whether the current user is allowed to see it, then I'd say render it and just hide it initially. Keeping it collapsed just lets the user not think about that detail unless they want it for certain records.
If it's big, or expensive to retrieve/calculate, or there are rules prohibiting some users from seeing certain details, that's different. In that case, I'd render only the "button" to access it, and load the details via ajax when requested.