|
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