I don't really understand how track by works and what it does.
My main goal is to use it with ng-repeat to add some precision.
Using track by to track strings & duplicate values
Normally ng-repeat tracks each item by the item itself. For the given array objs = [ 'one', 'one', 2, 'five', 'string', 'foo'], ng-repeat attempts to track changes by each obj in the ng-repeat="obj in objs". The problem is that we have duplicate values and angular will throw an error. One way to solve that is to have angular track the objects by other means. For strings, track by $index is a good solution as you really haven't other means to track a string.
track by & triggering a digest & input focuses
You allude to the fact you're somewhat new to angular. A digest cycle occurs when angular performs an exhaustive check of each watched property in order to reflect any change to the correspodant view; often during a digest cycle it happens that your code modify other watched properties so the procedure needs to be performed again until angular detects no more changes.
For example: You click a button to update a model via ng-click, then you do somethings (i mean, the things you wrote in the callback to perform when an user makes a click), then angular trigger digest cycle in order to refresh the view. I'm not too articulate in explaining that so you should investigate further if that didn't clarify things.
So back to track by. Let's use an example:
call a service to return an array of objects
update an object within the array and save object
after save service, depending on what the API returns, you may:
replace the whole object OR
update a value on the existing object
reflect change in ng-repeat UI
How you track this object will determine how the UI reflects the change.
One of the most annoying UXs I've experienced is this. Say you have a table of objects, each cell has an input where you want to in-line edit those objects' properties. I want to change the value, then on-blur, save that object while moving to the next cell to edit while you might be waiting on the response. So this is an autosave type thing. Depending on how you setup your track by statement, you may lose current focus (e.g. the field you're currently editing) when the response gets written back into your array of objects.
When you add track by you basically tell angular to generate a single DOM element per data object in the given collection.
You can track by $index if your data source has duplicate identifiers.
If you do need to repeat duplicate items, you can substitute the default tracking behavior with your own using the track by expression.
Example:
[{id:1,name:'one'}, {id:1,name:'one too'}, {id:2,name:'two'}]
Try to use the duplicate values in ng-repeat, you will get an error such as:
Error: ngRepeat:dupes Duplicate Key in Repeater
To avoid this kind of problems you should use track by $index. For example:
<ul>
<li ng-repeat="item in [1, 2, 3, 3] track by $index">
{{ item }}
</li>
</ul>
Here is how you would get $index in nested ng-repeat:
<div ng-repeat="row in matrix">
<div ng-repeat="column in row">
<span>outer: {{$parent.$index}} inner: {{$index}}</span>
</div>
</div>
Here are some resources that may help you:
track by $index documentation
ngRepeat documentation
2014 codelord.net article about ng-repeat performance and track by
You should use track by only if you need to go against the default behaviour of ng-repeat which is to remove duplicate items.
You can track the items using the scope property $index or specifying a custom function.
For instance:
<div ng-repeat="x in [42, 42, 43, 43] track by $index">
{{x}}
</div>
Display all values of the array (42 is displayed twice).
For reference: https://docs.angularjs.org/api/ng/directive/ngRepeat
Let's say, we have the following list:
<ul>
<li ng-repeat="item in items">
{{ item }}
</li>
</ul>
where, an item has the following structure:
{ 'id'=>id, 'name'=>name, 'description'=>description }
There is no problem whatsoever in this list until we wish to update it. For our own convenience we replace the list of items, with another updated list of items, as such:
items = newItems;
However, in this new list, few items change. Most items remain the same. Unfortunately Angular does not know how to identify our items and map them to the respective <li> elements, so it just deletes all elements and creates them again. This is extremely performance-costly in some cases and here is where track by comes in use.
By adding the track by clause to the element
<li ng-repeat="item in items track by item.id">
we are informing Angular, that the unique identifier of our items is item.id. Now Angular knows not to recreate all items, but only items with new ids, and only updates the rest. The performance improvement is significant in most cases. Also, personally, I like that I can monitor my items easier on my browser's developer tools, because they don't disappear every time I update them.
Related
Could someone explain what is the difference between those 2 v-for structures:
<li v-for="item in items" :key="item">
</li>
and
<li v-for="(item, i) in items" :key="i">
</li>
Vue requires all items inside a v-for to be "key-ed". The key is used to uniquely identify each element. This doesn't mean Vue will break if you don't use a key. It will warn it might not be able to detect all changes.
The "key" is particularly useful and important to Vue, as it allows it to skip re-rendering items which have not changed.
When duplicate keys are detected across a rendered collection, Vue will issue a warning.
Another Vue recommendation is that the "key"s are primitives (strings or numbers). When you specify non-primitive keys Vue will, again, issue a warning.
Given all the above recommendations, when rendering an array of unique primitives, using
<div v-for="item in items" :key="item" />
...is perfectly acceptable, as it meets the requirements: each key is primitive and unique.
Therefore, if you change the order of the items, Vue will be able to re-use the existing DOM elements and perform any move transitions (if you specified any). Testing this is actually fun. Consider this example, taken from the Vue documentation. Open it and, using dev-tools, change any of the rendered items' color to red. Then click the shuffle button. You'll see Vue re-uses the element and your custom change is kept, as the item is moved around.
When you're dealing with collections of non-primitives, or with collections of non-unique primitives, Vue still expects you to provide a unique primitive key for each item. Ideally, you should have a unique identifier (e.g: item.id). This is the typical solution and meets all requirements.
Sometimes, you don't have a unique identifier on each item, and an easy solution is to just use the items' position in the array as identifier (key):
<div v-for="(item, index) in items" :key="index" />
However, remember this can become problematic, particularly in cases where you change the order of the items after they're rendered and expect Vue to react to this change. It won't! Because Vue only watches the keys. When you swap items, keys don't change. only values do, so Vue won't re-render.
Official docs here:
Vue2 list rendering: https://v2.vuejs.org/v2/guide/list.html
Vue3 list rendering: https://vuejs.org/guide/essentials/list.html#list-rendering
As an example of how useful and powerful keys are when rendering, have a look at this virtual scroller.
Open the dev tools, inspect any cell and then scroll, while keeping the devtools open. You'll notice rows and columns only get updated when they change (each element flashes swiftly when it gets updated).
If you look into the code, you'll notice keys are dynamic. So you'd expect the first <div> in rows to always render the contents of the first object in rows and swap contents whenever another object takes first place in the array.
Thanks to the keys, Vue keeps the same <div> throughout its journey across the screen and only discards it after it leaves the scrolling window. As you scroll up, new <div>s are prepended to parent and as you scroll down new <div>s are appended, while the top ones are discarded.
This allows scrolling 100k rows (and 1 billion cells) smoothly (well, only in theory; in practice - we're only rendering as many cells as the screen can fit + 1 extra row + 1 extra column).
In the first case, the v-for iterates over all elements of items. It assigns the :key to the item itself. This is not ideal if you have duplicate elements in items, because each :key should be unique.
In the second case, v-for also iterates over all elements of items, but it introduces another variable named i, which represents the numerical index of the item in items. This index is then assigned to :key. This is better, because the indexes can't be duplicate.
As the purpose of :key attribute is to give a hint for Vue virtual DOM algorithm about change detection happen. Essentially, it helps Vue identify what's changed and what hasn't.
To understand it better I am adding the uses of both the scenarios :
:key="item" - If item is having unique value. So that each iterated element will get assigned with unique key.
:key="index" - If item is not having unique value. Hence, to assigned unique key to each iterated element we are using index.
Included is an object array being used in an ng-repeat call
And here is the error
For the love of me I cannot seem to get why angular is treating these as duplicates.
Any help is much appreciated.
Angular is telling you it doesn't know how to differentiate the items in your list, so you must tell it which field in your objects make it unique. Click here for more documentation on track by
To do this you need to add track by to your ng-repeat statement. You can specify any field on the object such as yid.
<div ng-repeat="item in items track by item.yid">
...
</div>
However, if you didn't have any fields that tracked uniqueness, you can also track by the index of the item in the list using $index.
<div ng-repeat="item in items track by $index">
...
</div>
I'm building a directive (Angular 1.2) that will toggle between displaying two different lists - think of them as a list of "trending" items on the site and a list of items the user is "following". So we have a toggle that allows the user to choose which list is being displayed, with an ng-repeat below it showing the items.
There will be a good deal of overlap between these lists, and when the user toggles from one list to the other, I'd like items that are contained in both to transition from their places on the "outgoing" list to their places on the new one, rather than disappearing and reappearing.
My question isn't about how to achieve the actual animations (we're using ngAnimate), but about how I should structure the controller/data to. I'm thinking about my directive controller having a trendingList and a followingList (that contain the actual data items), and an activeList that points to whichever of the two are currently being displayed. So the ng-repeat is actually on activeList, and toggling is essentially:
$scope.toggleMode = function(){
if ($scope.mode == 'trending')
$scope.mode = following;
$scope.activeList = followingList;
else {/*the inverse...*/}
}
Is that the most reasonable approach? If so, how do I ensure that angular recognizes the equality of objects present in both lists?
Or is there an easier/cleaner way to do this?
From what I understand, angular isn't tracking the equality of objects in the list it is repeating. The animations are triggered when the DOM is manipulated, not when the data changes (though data changes will change the DOM). As to what you should do, I don't have experience with this problem so hopefully somebody can point you in the right direction!
Let's suppose a list of 1000 items displayed with infinite scrolling.
Each item displays: a person's firstName, lastName, and mood. (to make it simple)
Initially, I didn't want to listen for updates.
So the great angular-bindonce directive or even better: angular 1.3 one-binding feature made the trick.
Now, I created a pull-to-refresh component, allowing to refresh the whole items.
However, as binding once, (and not reloading the page) my whole list didn't take the updates in account.
Using angular-bindonce, I have this currently:
<div bindonce ng-repeat="person in persons track by person.id">
<span bo-text="person.firstName"></span>
<span bo-text="person.lastName"></span>
<span bo-text="person.currentMood"></span>
</div>
The pull-to-refresh triggers this function:
$scope.refresh() {
Persons.getList(function(response)) {
$scope.persons = response.data; //data being an array
}
}
Question is:
Is there a way to refresh all the data ONLY when the pull-to-refresh is triggered?
In this case, I would be able to keep this one-binding that would greatly improve performance when dealing with huge lists of persons.
Until now, I'm forced to....use two-way binding, the natural way of Angular works.
More generally, how to deal with huge lists with infinite scrolling that needs to be updated only when some events are triggered?
Get angular-bind-notifier.
Use native bindings (with a somewhat modified syntax) and setup your markup like so:
<div ng-repeat="person in persons track by person.id" bind-notifier="{ eventKey:watchedExpression }">
<span>{{:eventKey:person.firstName}}</span>
<span>{{:eventKey:person.lastName}}</span>
<!-- or with ng-bind if you prefer that -->
<span ng-bind=":eventKey:person.currentMood"></span>
</div>
Now, whenever the value of watchedExpression changes - a $broadcast will be sent down through the childscope created by bind-notifier and tell every binding with the :key:expr syntax to re-evaluate.
If you need to, you can also send the $broadcast manually in the following format:
$scope.$broadcast('$$rebind::' + key) // where 'key' === 'eventKey' in the example above.
refresh-on directive could do the trick, found a reference HERE:
<div bindonce="persons" refresh-on="'refresh'" ng-repeat="person in persons track by person.id">
<span bo-text="person.firstName"></span>
<span bo-text="person.lastName"></span>
<span bo-text="person.currentMood"></span>
</div>
Instead of trying to work around not using two-way binding but still have all of its benefits there is more likely and easier solution. You say that there are 1,000 rows, are all 1,000 rows with the viewport / visible to the user at once?
I would assume not, so I would suggest using a buffered view for the list of items. Buffering the rows would mean that the rows that are not visible have no bindings but still take up space in the DOM so the scroll bar is always accurate.
The one major caveat of buffering is that all rows should be the same height, no variable height rows.
Here are some virtual scrolling / buffering directives to take a look at:
https://github.com/EnzeyNet/VirtualScroll
https://github.com/stackfull/angular-virtual-scroll
https://github.com/kamilkp/angular-vs-repeat
I'm having accordion which is populated dynamically.
<accordion-group ng-repeat="data in dataList" is-open="isAccordionOpen(data.dataName)">
<accordion-heading>
<span ng-click="openedAccordionGroup(data.dataName)" class="accordionSpan"><b>{{data.dataName}}</b>
</span>
</accordion-heading>
</accordion-group>
Here dataList keep on changing after 15 sec that means I'm populate dataList after regular inteval of 15sec. So i need to persist the last opened accordion group.
DataList can be very hude. So i cant parse and modify it to avoid method invocation in is-open attribute.
In js file, 'm having following code.
$scope.openedAccordionName = '';
$scope.isAccordionOpen = function(name){
return $scope.openedAccordionName === name;
};
$scope.openedAccordionGroup = function(name) {
$scope.openedAccordionName = name;
};
When I'm running it, its giving javascript error.
Error: [$compile:nonassign] Expression 'isAccordionOpen(data.dataName)' used with directive 'accordionGroup' is non-assignable!
What is wrong in above code?
You cannot really do that because angular is looking for a 2-way binding variable that it can assign to. You can easily maintain the last opened by using track by in your ng-repeat. With that what happens is angular will not recreate the DOM element, instead it will just update the existing element's scope which it identify based on what you are tracking by.
Here in the example i have an id for accordions so i am tracking it by id:-
<accordion-group ng-repeat="data in dataList track by data.id" is-open="isOpen">
Plnkr
By default if no track by provided angular will add a unique id $$hashKey for the repeated elements and since you are refreshing as a whole list it will remove the elements from DOM and recreate them. Using track by you will get better performance improvement as well. You can provide any unique key as trackby value (event dataName if it is unique).
In this example you can see that the last accordion is retained opened even if you refresh the data since the isOpen is added on the child scope of repeated element even if you refresh the data if will only update the data based on the id, it wont recreate the accordion.