Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 5789094

Browse files
committed
docs(ngRepeat): improve info about tracking
- deduplicate info between docs section and arguments - don't draw too much attention to track by $index ... - ... but highlight its drawbacks - add example to show how tracking affects collection updates - clarify duplicates support for specific tracking expressions Closes #16332 Closes #16334 Closes #16397
1 parent cf92c33 commit 5789094

File tree

1 file changed

+166
-75
lines changed

1 file changed

+166
-75
lines changed

src/ng/directive/ngRepeat.js

Lines changed: 166 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -74,71 +74,148 @@
7474
* For example, if an item is added to the collection, `ngRepeat` will know that all other items
7575
* already have DOM elements, and will not re-render them.
7676
*
77-
* The default tracking function (which tracks items by their identity) does not allow
78-
* duplicate items in arrays. This is because when there are duplicates, it is not possible
79-
* to maintain a one-to-one mapping between collection items and DOM elements.
80-
*
81-
* If you do need to repeat duplicate items, you can substitute the default tracking behavior
82-
* with your own using the `track by` expression.
83-
*
84-
* For example, you may track items by the index of each item in the collection, using the
85-
* special scope property `$index`:
86-
* ```html
87-
* <div ng-repeat="n in [42, 42, 43, 43] track by $index">
88-
* {{n}}
89-
* </div>
90-
* ```
91-
*
92-
* You may also use arbitrary expressions in `track by`, including references to custom functions
93-
* on the scope:
94-
* ```html
95-
* <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
96-
* {{n}}
97-
* </div>
98-
* ```
77+
* All different types of tracking functions, their syntax, and and their support for duplicate
78+
* items in collections can be found in the
79+
* {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
9980
*
10081
* <div class="alert alert-success">
101-
* If you are working with objects that have a unique identifier property, you should track
102-
* by this identifier instead of the object instance. Should you reload your data later, `ngRepeat`
103-
* will not have to rebuild the DOM elements for items it has already rendered, even if the
104-
* JavaScript objects in the collection have been substituted for new ones. For large collections,
105-
* this significantly improves rendering performance. If you don't have a unique identifier,
106-
* `track by $index` can also provide a performance boost.
82+
* **Best Practice:** If you are working with objects that have a unique identifier property, you
83+
* should track by this identifier instead of the object instance,
84+
* e.g. `item in items track by item.id`.
85+
* Should you reload your data later, `ngRepeat` will not have to rebuild the DOM elements for items
86+
* it has already rendered, even if the JavaScript objects in the collection have been substituted
87+
* for new ones. For large collections, this significantly improves rendering performance.
10788
* </div>
10889
*
109-
* ```html
110-
* <div ng-repeat="model in collection track by model.id">
111-
* {{model.name}}
112-
* </div>
113-
* ```
90+
* ### Effects of DOM Element re-use
11491
*
115-
* <br />
116-
* <div class="alert alert-warning">
117-
* Avoid using `track by $index` when the repeated template contains
118-
* {@link guide/expression#one-time-binding one-time bindings}. In such cases, the `nth` DOM
119-
* element will always be matched with the `nth` item of the array, so the bindings on that element
120-
* will not be updated even when the corresponding item changes, essentially causing the view to get
121-
* out-of-sync with the underlying data.
122-
* </div>
92+
* When DOM elements are re-used, ngRepeat updates the scope for the element, which will
93+
* automatically update any active bindings on the template. However, other
94+
* functionality will not be updated, because the element is not re-created:
12395
*
124-
* When no `track by` expression is provided, it is equivalent to tracking by the built-in
125-
* `$id` function, which tracks items by their identity:
126-
* ```html
127-
* <div ng-repeat="obj in collection track by $id(obj)">
128-
* {{obj.prop}}
129-
* </div>
130-
* ```
96+
* - Directives are not re-compiled
97+
* - {@link guide/expression#one-time-binding one-time expressions} on the repeated template are not
98+
* updated if they have stabilized.
13199
*
132-
* <br />
133-
* <div class="alert alert-warning">
134-
* **Note:** `track by` must always be the last expression:
135-
* </div>
136-
* ```
137-
* <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
138-
* {{model.name}}
139-
* </div>
140-
* ```
100+
* The above affects all kinds of element re-use due to tracking, but may be especially visible
101+
* when tracking by `$index` due to the way ngRepeat re-uses elements.
141102
*
103+
* The following example shows the effects of different actions with tracking:
104+
105+
<example module="ngRepeat" name="ngRepeat-tracking" deps="angular-animate.js" animations="true">
106+
<file name="script.js">
107+
angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
108+
var friends = [
109+
{name:'John', age:25},
110+
{name:'Mary', age:40},
111+
{name:'Peter', age:85}
112+
];
113+
114+
$scope.removeFirst = function() {
115+
$scope.friends.shift();
116+
};
117+
118+
$scope.updateAge = function() {
119+
$scope.friends.forEach(function(el) {
120+
el.age = el.age + 5;
121+
});
122+
};
123+
124+
$scope.copy = function() {
125+
$scope.friends = angular.copy($scope.friends);
126+
};
127+
128+
$scope.reset = function() {
129+
$scope.friends = angular.copy(friends);
130+
};
131+
132+
$scope.reset();
133+
});
134+
</file>
135+
<file name="index.html">
136+
<div ng-controller="repeatController">
137+
<ol>
138+
<li>When you click "Update Age", only the first list updates the age, because all others have
139+
a one-time binding on the age property. If you then click "Copy", the current friend list
140+
is copied, and now the second list updates the age, because the identity of the collection items
141+
has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the
142+
items are already known according to their tracking functions.
143+
</li>
144+
<li>When you click "Remove First", the 4th list has the wrong age on both remaining items. This is
145+
due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first
146+
DOM element for the new first collection item, and so on. Since the age property is one-time
147+
bound, the value remains from the collection item which was previously at this index.
148+
</li>
149+
</ol>
150+
151+
<button ng-click="removeFirst()">Remove First</button>
152+
<button ng-click="updateAge()">Update Age</button>
153+
<button ng-click="copy()">Copy</button>
154+
<br><button ng-click="reset()">Reset List</button>
155+
<br>
156+
<code>track by $id(friend)</code> (default):
157+
<ul class="example-animate-container">
158+
<li class="animate-repeat" ng-repeat="friend in friends">
159+
{{friend.name}} is {{friend.age}} years old.
160+
</li>
161+
</ul>
162+
<code>track by $id(friend)</code> (default), with age one-time binding:
163+
<ul class="example-animate-container">
164+
<li class="animate-repeat" ng-repeat="friend in friends">
165+
{{friend.name}} is {{::friend.age}} years old.
166+
</li>
167+
</ul>
168+
<code>track by friend.name</code>, with age one-time binding:
169+
<ul class="example-animate-container">
170+
<li class="animate-repeat" ng-repeat="friend in friends track by friend.name">
171+
{{friend.name}} is {{::friend.age}} years old.
172+
</li>
173+
</ul>
174+
<code>track by $index</code>, with age one-time binding:
175+
<ul class="example-animate-container">
176+
<li class="animate-repeat" ng-repeat="friend in friends track by $index">
177+
{{friend.name}} is {{::friend.age}} years old.
178+
</li>
179+
</ul>
180+
</div>
181+
</file>
182+
<file name="animations.css">
183+
.example-animate-container {
184+
background:white;
185+
border:1px solid black;
186+
list-style:none;
187+
margin:0;
188+
padding:0 10px;
189+
}
190+
191+
.animate-repeat {
192+
line-height:30px;
193+
list-style:none;
194+
box-sizing:border-box;
195+
}
196+
197+
.animate-repeat.ng-move,
198+
.animate-repeat.ng-enter,
199+
.animate-repeat.ng-leave {
200+
transition:all linear 0.5s;
201+
}
202+
203+
.animate-repeat.ng-leave.ng-leave-active,
204+
.animate-repeat.ng-move,
205+
.animate-repeat.ng-enter {
206+
opacity:0;
207+
max-height:0;
208+
}
209+
210+
.animate-repeat.ng-leave,
211+
.animate-repeat.ng-move.ng-move-active,
212+
.animate-repeat.ng-enter.ng-enter-active {
213+
opacity:1;
214+
max-height:30px;
215+
}
216+
</file>
217+
</example>
218+
142219
*
143220
* ## Special repeat start and end points
144221
* 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 @@
215292
* more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
216293
* mapped to the same DOM element, which is not possible.)
217294
*
218-
* <div class="alert alert-warning">
219-
* <strong>Note:</strong> the `track by` expression must come last - after any filters, and the alias expression.
220-
* </div>
295+
* *Default tracking: $id()*: `item in items` is equivalent to `item in items track by $id(item)`.
296+
* This implies that the DOM elements will be associated by item identity in the collection.
221297
*
222-
* For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
223-
* will be associated by item identity in the array.
298+
* The built-in `$id()` function can be used to assign a unique
299+
* `$$hashKey` property to each item in the collection. This property is then used as a key to associated DOM elements
300+
* with the corresponding item in the collection by identity. Moving the same object would move
301+
* the DOM element in the same way in the DOM.
302+
* Note that the default id function does not support duplicate primitive values (`number`, `string`),
303+
* but supports duplictae non-primitive values (`object`) that are *equal* in shape.
224304
*
225-
* For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
226-
* `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
227-
* with the corresponding item in the array by identity. Moving the same object in array would move the DOM
228-
* element in the same way in the DOM.
305+
* *Custom Expression*: It is possible to use any AngularJS expression to compute the tracking
306+
* id, for example with a function, or using a property on the collection items.
307+
* `item in items track by item.id` is a typical pattern when the items have a unique identifier,
308+
* e.g. database id. In this case the object identity does not matter. Two objects are considered
309+
* equivalent as long as their `id` property is same.
310+
* Tracking by unique identifier is the most performant way and should be used whenever possible.
229311
*
230-
* For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
231-
* case the object identity does not matter. Two objects are considered equivalent as long as their `id`
232-
* property is same.
312+
* *$index*: This special property tracks the collection items by their index, and
313+
* re-uses the DOM elements that match that index, e.g. `item in items track by $index`. This can
314+
* be used for a performance improvement if no unique identfier is available and the identity of
315+
* the collection items cannot be easily computed. It also allows duplicates.
233316
*
234-
* For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
235-
* to items in conjunction with a tracking expression.
317+
* <div class="alert alert-warning">
318+
* <strong>Note:</strong> Re-using DOM elements can have unforeseen effects. Read the
319+
* {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for
320+
* more info.
321+
* </div>
322+
*
323+
* <div class="alert alert-warning">
324+
* <strong>Note:</strong> the `track by` expression must come last - after any filters, and the alias expression:
325+
* `item in items | filter:searchText as results track by item.id`
326+
* </div>
236327
*
237328
* * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
238329
* intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
@@ -241,10 +332,10 @@
241332
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
242333
* the items have been processed through the filter.
243334
*
244-
* 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
245-
* (and not as operator, inside an expression).
335+
* Please note that `as [variable name] is not an operator but rather a part of ngRepeat
336+
* micro-syntax so it can be used only after all filters (and not as operator, inside an expression).
246337
*
247-
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
338+
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results track by item.id` .
248339
*
249340
* @example
250341
* This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
@@ -255,7 +346,7 @@
255346
I have {{friends.length}} friends. They are:
256347
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
257348
<ul class="example-animate-container">
258-
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
349+
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results track by friend.name">
259350
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
260351
</li>
261352
<li class="animate-repeat" ng-if="results.length === 0">

0 commit comments

Comments
 (0)