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

Commit 61f2767

Browse files
committed
feat(ngRepeat): add support for custom tracking of items
BREAKING CHANGE: It is considered an error to have two items produce the same track by key. (This was tolerated before.)
1 parent 5eb9685 commit 61f2767

File tree

5 files changed

+346
-352
lines changed

5 files changed

+346
-352
lines changed

src/apis.js

-44
Original file line numberDiff line numberDiff line change
@@ -65,47 +65,3 @@ HashMap.prototype = {
6565
return value;
6666
}
6767
};
68-
69-
/**
70-
* A map where multiple values can be added to the same key such that they form a queue.
71-
* @returns {HashQueueMap}
72-
*/
73-
function HashQueueMap() {}
74-
HashQueueMap.prototype = {
75-
/**
76-
* Same as array push, but using an array as the value for the hash
77-
*/
78-
push: function(key, value) {
79-
var array = this[key = hashKey(key)];
80-
if (!array) {
81-
this[key] = [value];
82-
} else {
83-
array.push(value);
84-
}
85-
},
86-
87-
/**
88-
* Same as array shift, but using an array as the value for the hash
89-
*/
90-
shift: function(key) {
91-
var array = this[key = hashKey(key)];
92-
if (array) {
93-
if (array.length == 1) {
94-
delete this[key];
95-
return array[0];
96-
} else {
97-
return array.shift();
98-
}
99-
}
100-
},
101-
102-
/**
103-
* return the first item without deleting it
104-
*/
105-
peek: function(key) {
106-
var array = this[hashKey(key)];
107-
if (array) {
108-
return array[0];
109-
}
110-
}
111-
};

src/ng/directive/ngRepeat.js

+171-122
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* @element ANY
2121
* @scope
2222
* @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
2424
* formats are currently supported:
2525
*
2626
* * `variable in expression` – where variable is the user defined loop variable and `expression`
@@ -33,6 +33,24 @@
3333
*
3434
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
3535
*
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+
*
3654
* @example
3755
* This example initializes the scope to a list of names and
3856
* then uses `ngRepeat` to display every person:
@@ -57,133 +75,164 @@
5775
</doc:scenario>
5876
</doc:example>
5977
*/
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 + "'.");
11893
}
11994

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;
147147
} 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();
150156
}
151157

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+
}
170190
}
171-
}
172191

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();
181214
}
182-
}
183-
}
184215

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

Comments
 (0)