This repository was archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.4k
docs(ngRepeat): improve info about tracking #16397
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,71 +74,148 @@ | |
* For example, if an item is added to the collection, `ngRepeat` will know that all other items | ||
* already have DOM elements, and will not re-render them. | ||
* | ||
* The default tracking function (which tracks items by their identity) 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`: | ||
* ```html | ||
* <div ng-repeat="n in [42, 42, 43, 43] track by $index"> | ||
* {{n}} | ||
* </div> | ||
* ``` | ||
* | ||
* You may also use arbitrary expressions in `track by`, including references to custom functions | ||
* on the scope: | ||
* ```html | ||
* <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)"> | ||
* {{n}} | ||
* </div> | ||
* ``` | ||
* All different types of tracking functions, their syntax, and and their support for duplicate | ||
* items in collections can be found in the | ||
* {@link ngRepeat#ngRepeat-arguments ngRepeat expression descriotion}. | ||
* | ||
* <div class="alert alert-success"> | ||
* 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. If you don't have a unique identifier, | ||
* `track by $index` can also provide a performance boost. | ||
* **Best Practice:** If you are working with objects that have a unique identifier property, you | ||
* should track by this identifier instead of the object instance, | ||
* e.g. `item in items track by item.id`. | ||
* 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. | ||
* </div> | ||
* | ||
* ```html | ||
* <div ng-repeat="model in collection track by model.id"> | ||
* {{model.name}} | ||
* </div> | ||
* ``` | ||
* ### Effects of DOM Element re-use | ||
* | ||
* <br /> | ||
* <div class="alert alert-warning"> | ||
* Avoid using `track by $index` when the repeated template contains | ||
* {@link guide/expression#one-time-binding 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. | ||
* </div> | ||
* When DOM elements are re-used, ngRepeat updates the scope for the element, which will | ||
* automatically update any active bindings on the template. However, other | ||
* functionality will not be updated, because it is a static at this point: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "a static" is a bit ambiguous here. Perhaps we could say "because the element is not recreated and recompiled" |
||
* | ||
* When no `track by` expression is provided, it is equivalent to tracking by the built-in | ||
* `$id` function, which tracks items by their identity: | ||
* ```html | ||
* <div ng-repeat="obj in collection track by $id(obj)"> | ||
* {{obj.prop}} | ||
* </div> | ||
* ``` | ||
* - {@link guide/expression#one-time-binding one-time expressions} on the repeated template are not | ||
* updated if they have stabilized. | ||
* - Directives are not re-compiled. | ||
* | ||
* <br /> | ||
* <div class="alert alert-warning"> | ||
* **Note:** `track by` must always be the last expression: | ||
* </div> | ||
* ``` | ||
* <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id"> | ||
* {{model.name}} | ||
* </div> | ||
* ``` | ||
* The above affects all kinds of element re-use due to tracking, but may be especially visible | ||
* when tracking by `$index` due to the way ngRepeat re-uses elements. | ||
* | ||
* The following example shows the effects of different actions with tracking: | ||
|
||
<example module="ngRepeat" name="ngRepeat-tracking" deps="angular-animate.js" animations="true"> | ||
<file name="script.js"> | ||
angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) { | ||
var friends = [ | ||
{name:'John', age:25}, | ||
{name:'Mary', age:40}, | ||
{name:'Peter', age:85} | ||
]; | ||
|
||
$scope.removeFirst = function() { | ||
$scope.friends.shift(); | ||
}; | ||
|
||
$scope.updateAge = function() { | ||
$scope.friends.forEach(function(el) { | ||
el.age = el.age + 5; | ||
}); | ||
}; | ||
|
||
$scope.copy = function() { | ||
$scope.friends = angular.copy($scope.friends); | ||
}; | ||
|
||
$scope.reset = function() { | ||
$scope.friends = angular.copy(friends); | ||
}; | ||
|
||
$scope.reset(); | ||
}); | ||
</file> | ||
<file name="index.html"> | ||
<div ng-controller="repeatController"> | ||
<ol> | ||
<li>When you click "Update Age", only the first list updates the age, because all others have | ||
a one-time binding on the age property. If you then click "Copy", the current friend list | ||
is copied, and now the second list updates the age, because the identity of the collection items | ||
has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the | ||
items are already known according to their tracking functions. | ||
</li> | ||
<li>When you click "Remove First", the 4th list has the wrong age on both remaining items. This is | ||
due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first | ||
DOM element for the new first collection item, and so on. Since the age property is one-time | ||
bound, the value remains from the collection item which was previously at this index. | ||
</li> | ||
</ol> | ||
|
||
<button ng-click="removeFirst()">Remove First</button> | ||
<button ng-click="updateAge()">Update Age</button> | ||
<button ng-click="copy()">Copy</button> | ||
<br><button ng-click="reset()">Reset List</button> | ||
<br> | ||
<code>track by $id(friend)</code> (default): | ||
<ul class="example-animate-container"> | ||
<li class="animate-repeat" ng-repeat="friend in friends"> | ||
{{friend.name}} is {{friend.age}} years old. | ||
</li> | ||
</ul> | ||
<code>track by $id(friend)</code> (default), with age one-time binding: | ||
<ul class="example-animate-container"> | ||
<li class="animate-repeat" ng-repeat="friend in friends"> | ||
{{friend.name}} is {{::friend.age}} years old. | ||
</li> | ||
</ul> | ||
<code>track by friend.name</code>, with age one-time binding: | ||
<ul class="example-animate-container"> | ||
<li class="animate-repeat" ng-repeat="friend in friends track by friend.name"> | ||
{{friend.name}} is {{::friend.age}} years old. | ||
</li> | ||
</ul> | ||
<code>track by $index</code>, with age one-time binding: | ||
<ul class="example-animate-container"> | ||
<li class="animate-repeat" ng-repeat="friend in friends track by $index"> | ||
{{friend.name}} is {{::friend.age}} years old. | ||
</li> | ||
</ul> | ||
</div> | ||
</file> | ||
<file name="animations.css"> | ||
.example-animate-container { | ||
background:white; | ||
border:1px solid black; | ||
list-style:none; | ||
margin:0; | ||
padding:0 10px; | ||
} | ||
|
||
.animate-repeat { | ||
line-height:30px; | ||
list-style:none; | ||
box-sizing:border-box; | ||
} | ||
|
||
.animate-repeat.ng-move, | ||
.animate-repeat.ng-enter, | ||
.animate-repeat.ng-leave { | ||
transition:all linear 0.5s; | ||
} | ||
|
||
.animate-repeat.ng-leave.ng-leave-active, | ||
.animate-repeat.ng-move, | ||
.animate-repeat.ng-enter { | ||
opacity:0; | ||
max-height:0; | ||
} | ||
|
||
.animate-repeat.ng-leave, | ||
.animate-repeat.ng-move.ng-move-active, | ||
.animate-repeat.ng-enter.ng-enter-active { | ||
opacity:1; | ||
max-height:30px; | ||
} | ||
</file> | ||
</example> | ||
|
||
* | ||
* ## Special repeat start and end points | ||
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending | ||
|
@@ -215,24 +292,38 @@ | |
* more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are | ||
* mapped to the same DOM element, which is not possible.) | ||
* | ||
* <div class="alert alert-warning"> | ||
* <strong>Note:</strong> the `track by` expression must come last - after any filters, and the alias expression. | ||
* </div> | ||
* *Default tracking: $id()*: `item in items` is equivalent to `item in items track by $id(item)`. | ||
* This implies that the DOM elements will be associated by item identity in the collection. | ||
* | ||
* For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements | ||
* will be associated by item identity in the array. | ||
* The built-in `$id()` function can be used to assign a unique | ||
* `$$hashKey` property to each item in the collection. This property is then used as a key to associated DOM elements | ||
* with the corresponding item in the collection by identity. Moving the same object would move | ||
* the DOM element in the same way in the DOM. | ||
* Note that the default id function does not support duplicate primitive values (`number`, `string`), | ||
* but supports duplictae non-primitive values (`object`) that are *equal* in shape. | ||
* | ||
* For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique | ||
* `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements | ||
* with the corresponding item in the array by identity. Moving the same object in array would move the DOM | ||
* element in the same way in the DOM. | ||
* *Custom Expression*: It is possible to use any AngularJS expression to compute the tracking | ||
* id, for example with a function, or using a property on the collection items. | ||
* `item in items track by item.id` is a typical pattern when the items have a unique identifier, | ||
* e.g. database id. In this case the object identity does not matter. Two objects are considered | ||
* equivalent as long as their `id` property is same. | ||
* Tracking by unique identifier is the most performant way and should be used whenever possible. | ||
* | ||
* For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this | ||
* case the object identity does not matter. Two objects are considered equivalent as long as their `id` | ||
* property is same. | ||
* *$index*: This special property tracks the collection items by their index, and | ||
* re-uses the DOM elements that match that index, e.g. `item in items track by $index`. This can | ||
* be used for a performance improvement if no unique identfier is available and the identity of | ||
* the collection items cannot be easily computed. It also allows duplicates. | ||
* | ||
* For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter | ||
* to items in conjunction with a tracking expression. | ||
* <div class="alert alert-warning"> | ||
* <strong>Note:</strong> Re-using DOM elements can have unforeseen effects. Read the | ||
* {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for | ||
* more info. | ||
* </div> | ||
* | ||
* <div class="alert alert-warning"> | ||
* <strong>Note:</strong> the `track by` expression must come last - after any filters, and the alias expression: | ||
* `item in items | filter:searchText as results track by item.id` | ||
* </div> | ||
* | ||
* * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the | ||
* intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message | ||
|
@@ -241,10 +332,10 @@ | |
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after | ||
* the items have been processed through the filter. | ||
* | ||
* Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end | ||
* (and not as operator, inside an expression). | ||
* Please note that `as [variable name] is not an operator but rather a part of ngRepeat | ||
* micro-syntax so it can be used only after all filters (and not as operator, inside an expression). | ||
* | ||
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` . | ||
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results track by item.id` . | ||
* | ||
* @example | ||
* This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed | ||
|
@@ -255,7 +346,7 @@ | |
I have {{friends.length}} friends. They are: | ||
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" /> | ||
<ul class="example-animate-container"> | ||
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> | ||
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results track by friend.name"> | ||
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. | ||
</li> | ||
<li class="animate-repeat" ng-if="results.length === 0"> | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: description