|
20 | 20 | * @element ANY |
21 | 21 | * @scope |
22 | 22 | * @priority 1000 |
23 | | - * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two |
| 23 | + * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These |
24 | 24 | * formats are currently supported: |
25 | 25 | * |
26 | 26 | * * `variable in expression` – where variable is the user defined loop variable and `expression` |
|
33 | 33 | * |
34 | 34 | * For example: `(name, age) in {'adam':10, 'amalie':12}`. |
35 | 35 | * |
| 36 | + * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function |
| 37 | + * which can be used to associate the objects in the collection with the DOM elements. If no tractking function |
| 38 | + * is specified the ng-repeat associates elements by identity in the collection. It is an error to have |
| 39 | + * more then one tractking function to resolve to the same key. (This would mean that two distinct objects are |
| 40 | + * mapped to the same DOM element, which is not possible.) |
| 41 | + * |
| 42 | + * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements |
| 43 | + * will be associated by item identity in the array. |
| 44 | + * |
| 45 | + * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique |
| 46 | + * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements |
| 47 | + * with the corresponding item in the array by identity. Moving the same object in array would move the DOM |
| 48 | + * element in the same way ian the DOM. |
| 49 | + * |
| 50 | + * For example: `item in items track by item.id` Is a typical pattern when the items come from the database. In this |
| 51 | + * case the object identity does not matter. Two objects are considered equivalent as long as their `id` |
| 52 | + * property is same. |
| 53 | + * |
36 | 54 | * @example |
37 | 55 | * This example initializes the scope to a list of names and |
38 | 56 | * then uses `ngRepeat` to display every person: |
|
57 | 75 | </doc:scenario> |
58 | 76 | </doc:example> |
59 | 77 | */ |
60 | | -var ngRepeatDirective = ngDirective({ |
61 | | - transclude: 'element', |
62 | | - priority: 1000, |
63 | | - terminal: true, |
64 | | - compile: function(element, attr, linker) { |
65 | | - return function(scope, iterStartElement, attr){ |
66 | | - var expression = attr.ngRepeat; |
67 | | - var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), |
68 | | - lhs, rhs, valueIdent, keyIdent; |
69 | | - if (! match) { |
70 | | - throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" + |
71 | | - expression + "'."); |
72 | | - } |
73 | | - lhs = match[1]; |
74 | | - rhs = match[2]; |
75 | | - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); |
76 | | - if (!match) { |
77 | | - throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + |
78 | | - lhs + "'."); |
79 | | - } |
80 | | - valueIdent = match[3] || match[1]; |
81 | | - keyIdent = match[2]; |
82 | | - |
83 | | - // Store a list of elements from previous run. This is a hash where key is the item from the |
84 | | - // iterator, and the value is an array of objects with following properties. |
85 | | - // - scope: bound scope |
86 | | - // - element: previous element. |
87 | | - // - index: position |
88 | | - // We need an array of these objects since the same object can be returned from the iterator. |
89 | | - // We expect this to be a rare case. |
90 | | - var lastOrder = new HashQueueMap(); |
91 | | - |
92 | | - scope.$watch(function ngRepeatWatch(scope){ |
93 | | - var index, length, |
94 | | - collection = scope.$eval(rhs), |
95 | | - cursor = iterStartElement, // current position of the node |
96 | | - // Same as lastOrder but it has the current state. It will become the |
97 | | - // lastOrder on the next iteration. |
98 | | - nextOrder = new HashQueueMap(), |
99 | | - arrayBound, |
100 | | - childScope, |
101 | | - key, value, // key/value of iteration |
102 | | - array, |
103 | | - last; // last object information {scope, element, index} |
104 | | - |
105 | | - |
106 | | - |
107 | | - if (!isArray(collection)) { |
108 | | - // if object, extract keys, sort them and use to determine order of iteration over obj props |
109 | | - array = []; |
110 | | - for(key in collection) { |
111 | | - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { |
112 | | - array.push(key); |
113 | | - } |
114 | | - } |
115 | | - array.sort(); |
116 | | - } else { |
117 | | - array = collection || []; |
| 78 | +var ngRepeatDirective = ['$parse', function($parse) { |
| 79 | + return { |
| 80 | + transclude: 'element', |
| 81 | + priority: 1000, |
| 82 | + terminal: true, |
| 83 | + compile: function(element, attr, linker) { |
| 84 | + return function($scope, $element, $attr){ |
| 85 | + var expression = $attr.ngRepeat; |
| 86 | + var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/), |
| 87 | + trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier, |
| 88 | + hashFnLocals = {$id: hashKey}; |
| 89 | + |
| 90 | + if (!match) { |
| 91 | + throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" + |
| 92 | + expression + "'."); |
118 | 93 | } |
119 | 94 |
|
120 | | - arrayBound = array.length-1; |
121 | | - |
122 | | - // we are not using forEach for perf reasons (trying to avoid #call) |
123 | | - for (index = 0, length = array.length; index < length; index++) { |
124 | | - key = (collection === array) ? index : array[index]; |
125 | | - value = collection[key]; |
126 | | - |
127 | | - last = lastOrder.shift(value); |
128 | | - |
129 | | - if (last) { |
130 | | - // if we have already seen this object, then we need to reuse the |
131 | | - // associated scope/element |
132 | | - childScope = last.scope; |
133 | | - nextOrder.push(value, last); |
134 | | - |
135 | | - if (index === last.index) { |
136 | | - // do nothing |
137 | | - cursor = last.element; |
138 | | - } else { |
139 | | - // existing item which got moved |
140 | | - last.index = index; |
141 | | - // This may be a noop, if the element is next, but I don't know of a good way to |
142 | | - // figure this out, since it would require extra DOM access, so let's just hope that |
143 | | - // the browsers realizes that it is noop, and treats it as such. |
144 | | - cursor.after(last.element); |
145 | | - cursor = last.element; |
146 | | - } |
| 95 | + lhs = match[1]; |
| 96 | + rhs = match[2]; |
| 97 | + trackByExp = match[4]; |
| 98 | + |
| 99 | + if (trackByExp) { |
| 100 | + hashExpFn = $parse(trackByExp); |
| 101 | + trackByIdFn = function(key, value, index) { |
| 102 | + // assign key, value, and $index to the locals so that they can be used in hash functions |
| 103 | + if (keyIdentifier) hashFnLocals[keyIdentifier] = key; |
| 104 | + hashFnLocals[valueIdentifier] = value; |
| 105 | + hashFnLocals.$index = index; |
| 106 | + return hashExpFn($scope, hashFnLocals); |
| 107 | + }; |
| 108 | + } else { |
| 109 | + trackByIdFn = function(key, value) { |
| 110 | + return hashKey(value); |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); |
| 115 | + if (!match) { |
| 116 | + throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + |
| 117 | + lhs + "'."); |
| 118 | + } |
| 119 | + valueIdentifier = match[3] || match[1]; |
| 120 | + keyIdentifier = match[2]; |
| 121 | + |
| 122 | + // Store a list of elements from previous run. This is a hash where key is the item from the |
| 123 | + // iterator, and the value is objects with following properties. |
| 124 | + // - scope: bound scope |
| 125 | + // - element: previous element. |
| 126 | + // - index: position |
| 127 | + var lastBlockMap = {}; |
| 128 | + |
| 129 | + //watch props |
| 130 | + $scope.$watchCollection(rhs, function ngRepeatAction(collection){ |
| 131 | + var index, length, |
| 132 | + cursor = $element, // current position of the node |
| 133 | + // Same as lastBlockMap but it has the current state. It will become the |
| 134 | + // lastBlockMap on the next iteration. |
| 135 | + nextBlockMap = {}, |
| 136 | + arrayLength, |
| 137 | + childScope, |
| 138 | + key, value, // key/value of iteration |
| 139 | + trackById, |
| 140 | + collectionKeys, |
| 141 | + block, // last object information {scope, element, id} |
| 142 | + nextBlockOrder = []; |
| 143 | + |
| 144 | + |
| 145 | + if (isArray(collection)) { |
| 146 | + collectionKeys = collection; |
147 | 147 | } else { |
148 | | - // new item which we don't know about |
149 | | - childScope = scope.$new(); |
| 148 | + // if object, extract keys, sort them and use to determine order of iteration over obj props |
| 149 | + collectionKeys = []; |
| 150 | + for (key in collection) { |
| 151 | + if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { |
| 152 | + collectionKeys.push(key); |
| 153 | + } |
| 154 | + } |
| 155 | + collectionKeys.sort(); |
150 | 156 | } |
151 | 157 |
|
152 | | - childScope[valueIdent] = value; |
153 | | - if (keyIdent) childScope[keyIdent] = key; |
154 | | - childScope.$index = index; |
155 | | - |
156 | | - childScope.$first = (index === 0); |
157 | | - childScope.$last = (index === arrayBound); |
158 | | - childScope.$middle = !(childScope.$first || childScope.$last); |
159 | | - |
160 | | - if (!last) { |
161 | | - linker(childScope, function(clone){ |
162 | | - cursor.after(clone); |
163 | | - last = { |
164 | | - scope: childScope, |
165 | | - element: (cursor = clone), |
166 | | - index: index |
167 | | - }; |
168 | | - nextOrder.push(value, last); |
169 | | - }); |
| 158 | + arrayLength = collectionKeys.length; |
| 159 | + |
| 160 | + // locate existing items |
| 161 | + length = nextBlockOrder.length = collectionKeys.length; |
| 162 | + for(index = 0; index < length; index++) { |
| 163 | + key = (collection === collectionKeys) ? index : collectionKeys[index]; |
| 164 | + value = collection[key]; |
| 165 | + trackById = trackByIdFn(key, value, index); |
| 166 | + if((block = lastBlockMap[trackById])) { |
| 167 | + delete lastBlockMap[trackById]; |
| 168 | + nextBlockMap[trackById] = block; |
| 169 | + nextBlockOrder[index] = block; |
| 170 | + } else if (nextBlockMap.hasOwnProperty(trackById)) { |
| 171 | + // restore lastBlockMap |
| 172 | + forEach(nextBlockOrder, function(block) { |
| 173 | + if (block && block.element) lastBlockMap[block.id] = block; |
| 174 | + }); |
| 175 | + // This is a duplicate and we need to throw an error |
| 176 | + throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression); |
| 177 | + } else { |
| 178 | + // new never before seen block |
| 179 | + nextBlockOrder[index] = { id: trackById }; |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + // remove existing items |
| 184 | + for (key in lastBlockMap) { |
| 185 | + if (lastBlockMap.hasOwnProperty(key)) { |
| 186 | + block = lastBlockMap[key]; |
| 187 | + block.element.remove(); |
| 188 | + block.scope.$destroy(); |
| 189 | + } |
170 | 190 | } |
171 | | - } |
172 | 191 |
|
173 | | - //shrink children |
174 | | - for (key in lastOrder) { |
175 | | - if (lastOrder.hasOwnProperty(key)) { |
176 | | - array = lastOrder[key]; |
177 | | - while(array.length) { |
178 | | - value = array.pop(); |
179 | | - value.element.remove(); |
180 | | - value.scope.$destroy(); |
| 192 | + // we are not using forEach for perf reasons (trying to avoid #call) |
| 193 | + for (index = 0, length = collectionKeys.length; index < length; index++) { |
| 194 | + key = (collection === collectionKeys) ? index : collectionKeys[index]; |
| 195 | + value = collection[key]; |
| 196 | + block = nextBlockOrder[index]; |
| 197 | + |
| 198 | + if (block.element) { |
| 199 | + // if we have already seen this object, then we need to reuse the |
| 200 | + // associated scope/element |
| 201 | + childScope = block.scope; |
| 202 | + |
| 203 | + if (block.element == cursor) { |
| 204 | + // do nothing |
| 205 | + cursor = block.element; |
| 206 | + } else { |
| 207 | + // existing item which got moved |
| 208 | + cursor.after(block.element); |
| 209 | + cursor = block.element; |
| 210 | + } |
| 211 | + } else { |
| 212 | + // new item which we don't know about |
| 213 | + childScope = $scope.$new(); |
181 | 214 | } |
182 | | - } |
183 | | - } |
184 | 215 |
|
185 | | - lastOrder = nextOrder; |
186 | | - }); |
187 | | - }; |
188 | | - } |
189 | | -}); |
| 216 | + childScope[valueIdentifier] = value; |
| 217 | + if (keyIdentifier) childScope[keyIdentifier] = key; |
| 218 | + childScope.$index = index; |
| 219 | + childScope.$first = (index === 0); |
| 220 | + childScope.$last = (index === (arrayLength - 1)); |
| 221 | + childScope.$middle = !(childScope.$first || childScope.$last); |
| 222 | + |
| 223 | + if (!block.element) { |
| 224 | + linker(childScope, function(clone){ |
| 225 | + cursor.after(clone); |
| 226 | + cursor = clone; |
| 227 | + block.scope = childScope; |
| 228 | + block.element = clone; |
| 229 | + nextBlockMap[block.id] = block; |
| 230 | + }); |
| 231 | + } |
| 232 | + } |
| 233 | + lastBlockMap = nextBlockMap; |
| 234 | + }); |
| 235 | + }; |
| 236 | + } |
| 237 | + }; |
| 238 | +}]; |
0 commit comments