From bbf142798c4753e938cfecbbb2f31be4a6ff650f Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 9 Jan 2018 18:54:45 +0100 Subject: [PATCH 1/2] docs(ngRepeat): improve info about tracking - deduplicate info between docs section and arguments - don't draw too much attention to track by ... - ... but highlight its drawbacks when used with one-time bindings - add example to show how tracking affects collection updates - clarify duplicates support for specific tracking expressions Closes #16332 Closes #16334 --- src/ng/directive/ngRepeat.js | 241 ++++++++++++++++++++++++----------- 1 file changed, 166 insertions(+), 75 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index ca80f386c991..0f3f8f69df74 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -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 - *
- * {{n}} - *
- * ``` - * - * You may also use arbitrary expressions in `track by`, including references to custom functions - * on the scope: - * ```html - *
- * {{n}} - *
- * ``` + * 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}. * *
- * 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. *
* - * ```html - *
- * {{model.name}} - *
- * ``` + * ### Effects of DOM Element re-use * - *
- *
- * 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. - *
+ * 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: * - * 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 - *
- * {{obj.prop}} - *
- * ``` + * - {@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. * - *
- *
- * **Note:** `track by` must always be the last expression: - *
- * ``` - *
- * {{model.name}} - *
- * ``` + * 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: + + + + 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(); + }); + + +
+
    +
  1. 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. +
  2. +
  3. 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. +
  4. +
+ + + + +
+
+ track by $id(friend) (default): +
    +
  • + {{friend.name}} is {{friend.age}} years old. +
  • +
+ track by $id(friend) (default), with age one-time binding: +
    +
  • + {{friend.name}} is {{::friend.age}} years old. +
  • +
+ track by friend.name, with age one-time binding: +
    +
  • + {{friend.name}} is {{::friend.age}} years old. +
  • +
+ track by $index, with age one-time binding: +
    +
  • + {{friend.name}} is {{::friend.age}} years old. +
  • +
+
+
+ + .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; + } + +
+ * * ## 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.) * - *
- * Note: the `track by` expression must come last - after any filters, and the alias expression. - *
+ * *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. + *
+ * Note: Re-using DOM elements can have unforeseen effects. Read the + * {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for + * more info. + *
+ * + *
+ * Note: 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` + *
* * * `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: