|
74 | 74 | * For example, if an item is added to the collection, `ngRepeat` will know that all other items
|
75 | 75 | * already have DOM elements, and will not re-render them.
|
76 | 76 | *
|
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}. |
99 | 80 | *
|
100 | 81 | * <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. |
107 | 88 | * </div>
|
108 | 89 | *
|
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 |
114 | 91 | *
|
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: |
123 | 95 | *
|
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. |
131 | 99 | *
|
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. |
141 | 102 | *
|
| 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 | +
|
142 | 219 | *
|
143 | 220 | * ## Special repeat start and end points
|
144 | 221 | * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
|
|
215 | 292 | * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
|
216 | 293 | * mapped to the same DOM element, which is not possible.)
|
217 | 294 | *
|
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. |
221 | 297 | *
|
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. |
224 | 304 | *
|
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. |
229 | 311 | *
|
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. |
233 | 316 | *
|
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> |
236 | 327 | *
|
237 | 328 | * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
|
238 | 329 | * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
|
|
241 | 332 | * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
|
242 | 333 | * the items have been processed through the filter.
|
243 | 334 | *
|
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). |
246 | 337 | *
|
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` . |
248 | 339 | *
|
249 | 340 | * @example
|
250 | 341 | * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
|
|
255 | 346 | I have {{friends.length}} friends. They are:
|
256 | 347 | <input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
|
257 | 348 | <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"> |
259 | 350 | [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
|
260 | 351 | </li>
|
261 | 352 | <li class="animate-repeat" ng-if="results.length === 0">
|
|
0 commit comments