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

Commit 75f11f1

Browse files
mheveryIgorMinar
authored andcommitted
feat(ng:repeat) collection items and DOM elements affinity / stability
1 parent e134a83 commit 75f11f1

File tree

7 files changed

+384
-227
lines changed

7 files changed

+384
-227
lines changed

CHANGELOG.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@
5252
- If Angular is being used with jQuery older than 1.6, some features might not work properly. Please
5353
upgrade to jQuery version 1.6.4.
5454

55-
55+
## Breaking Changes
56+
- ng:repeat no longer has ng:repeat-index property. This is because the elements now have
57+
affinity to the underlying collection, and moving items around in the collection would move
58+
ng:repeat-index property rendering it meaningless.
5659

5760

5861
<a name="0.10.1"><a/>
@@ -88,7 +91,7 @@
8891
- $location.hashPath -> $location.path()
8992
- $location.hashSearch -> $location.search()
9093
- $location.search -> no equivalent, use $window.location.search (this is so that we can work in
91-
hashBang and html5 mode at the same time, check out the docs)
94+
hashBang and html5 mode at the same time, check out the docs)
9295
- $location.update() / $location.updateHash() -> use $location.url()
9396
- n/a -> $location.replace() - new api for replacing history record instead of creating a new one
9497

src/apis.js

+44-14
Original file line numberDiff line numberDiff line change
@@ -840,20 +840,22 @@ var angularFunction = {
840840
* Hash of a:
841841
* string is string
842842
* number is number as string
843-
* object is either call $hashKey function on object or assign unique hashKey id.
843+
* object is either result of calling $$hashKey function on the object or uniquely generated id,
844+
* that is also assigned to the $$hashKey property of the object.
844845
*
845846
* @param obj
846-
* @returns {String} hash string such that the same input will have the same hash string
847+
* @returns {String} hash string such that the same input will have the same hash string.
848+
* The resulting string key is in 'type:hashKey' format.
847849
*/
848850
function hashKey(obj) {
849851
var objType = typeof obj;
850852
var key = obj;
851853
if (objType == 'object') {
852-
if (typeof (key = obj.$hashKey) == 'function') {
854+
if (typeof (key = obj.$$hashKey) == 'function') {
853855
// must invoke on object to keep the right this
854-
key = obj.$hashKey();
856+
key = obj.$$hashKey();
855857
} else if (key === undefined) {
856-
key = obj.$hashKey = nextUid();
858+
key = obj.$$hashKey = nextUid();
857859
}
858860
}
859861
return objType + ':' + key;
@@ -868,13 +870,9 @@ HashMap.prototype = {
868870
* Store key value pair
869871
* @param key key to store can be any type
870872
* @param value value to store can be any type
871-
* @returns old value if any
872873
*/
873874
put: function(key, value) {
874-
var _key = hashKey(key);
875-
var oldValue = this[_key];
876-
this[_key] = value;
877-
return oldValue;
875+
this[hashKey(key)] = value;
878876
},
879877

880878
/**
@@ -888,16 +886,48 @@ HashMap.prototype = {
888886
/**
889887
* Remove the key/value pair
890888
* @param key
891-
* @returns value associated with key before it was removed
892889
*/
893890
remove: function(key) {
894-
var _key = hashKey(key);
895-
var value = this[_key];
896-
delete this[_key];
891+
var value = this[key = hashKey(key)];
892+
delete this[key];
897893
return value;
898894
}
899895
};
900896

897+
/**
898+
* A map where multiple values can be added to the same key such that the form a queue.
899+
* @returns {HashQueueMap}
900+
*/
901+
function HashQueueMap(){}
902+
HashQueueMap.prototype = {
903+
/**
904+
* Same as array push, but using an array as the value for the hash
905+
*/
906+
push: function(key, value) {
907+
var array = this[key = hashKey(key)];
908+
if (!array) {
909+
this[key] = [value];
910+
} else {
911+
array.push(value);
912+
}
913+
},
914+
915+
/**
916+
* Same as array shift, but using an array as the value for the hash
917+
*/
918+
shift: function(key) {
919+
var array = this[key = hashKey(key)];
920+
if (array) {
921+
if (array.length == 1) {
922+
delete this[key];
923+
return array[0];
924+
} else {
925+
return array.shift();
926+
}
927+
}
928+
}
929+
};
930+
901931
function defineApi(dst, chain){
902932
angular[dst] = angular[dst] || {};
903933
forEach(chain, function(parent){

src/widgets.js

+66-46
Original file line numberDiff line numberDiff line change
@@ -1182,10 +1182,9 @@ angularWidget('a', function() {
11821182
* @name angular.widget.@ng:repeat
11831183
*
11841184
* @description
1185-
* The `ng:repeat` widget instantiates a template once per item from a collection. The collection is
1186-
* enumerated with the `ng:repeat-index` attribute, starting from 0. Each template instance gets
1187-
* its own scope, where the given loop variable is set to the current collection item, and `$index`
1188-
* is set to the item index or key.
1185+
* The `ng:repeat` widget instantiates a template once per item from a collection. Each template
1186+
* instance gets its own scope, where the given loop variable is set to the current collection item,
1187+
* and `$index` is set to the item index or key.
11891188
*
11901189
* Special properties are exposed on the local scope of each template instance, including:
11911190
*
@@ -1256,68 +1255,89 @@ angularWidget('@ng:repeat', function(expression, element){
12561255
valueIdent = match[3] || match[1];
12571256
keyIdent = match[2];
12581257

1259-
var childScopes = [];
1260-
var childElements = [iterStartElement];
12611258
var parentScope = this;
1259+
// Store a list of elements from previous run. This is a hash where key is the item from the
1260+
// iterator, and the value is an array of objects with following properties.
1261+
// - scope: bound scope
1262+
// - element: previous element.
1263+
// - index: position
1264+
// We need an array of these objects since the same object can be returned from the iterator.
1265+
// We expect this to be a rare case.
1266+
var lastOrder = new HashQueueMap();
12621267
this.$watch(function(scope){
12631268
var index = 0,
1264-
childCount = childScopes.length,
12651269
collection = scope.$eval(rhs),
12661270
collectionLength = size(collection, true),
1267-
fragment = document.createDocumentFragment(),
1268-
addFragmentTo = (childCount < collectionLength) ? childElements[childCount] : null,
12691271
childScope,
1270-
key;
1272+
// Same as lastOrder but it has the current state. It will become the
1273+
// lastOrder on the next iteration.
1274+
nextOrder = new HashQueueMap(),
1275+
key, value, // key/value of iteration
1276+
array, last, // last object information {scope, element, index}
1277+
cursor = iterStartElement; // current position of the node
12711278

12721279
for (key in collection) {
12731280
if (collection.hasOwnProperty(key)) {
1274-
if (index < childCount) {
1275-
// reuse existing child
1276-
childScope = childScopes[index];
1277-
childScope[valueIdent] = collection[key];
1278-
if (keyIdent) childScope[keyIdent] = key;
1279-
childScope.$position = index == 0
1280-
? 'first'
1281-
: (index == collectionLength - 1 ? 'last' : 'middle');
1282-
childScope.$eval();
1281+
last = lastOrder.shift(value = collection[key]);
1282+
if (last) {
1283+
// if we have already seen this object, then we need to reuse the
1284+
// associated scope/element
1285+
childScope = last.scope;
1286+
nextOrder.push(value, last);
1287+
1288+
if (index === last.index) {
1289+
// do nothing
1290+
cursor = last.element;
1291+
} else {
1292+
// existing item which got moved
1293+
last.index = index;
1294+
// This may be a noop, if the element is next, but I don't know of a good way to
1295+
// figure this out, since it would require extra DOM access, so let's just hope that
1296+
// the browsers realizes that it is noop, and treats it as such.
1297+
cursor.after(last.element);
1298+
cursor = last.element;
1299+
}
12831300
} else {
1284-
// grow children
1301+
// new item which we don't know about
12851302
childScope = parentScope.$new();
1286-
childScope[valueIdent] = collection[key];
1287-
if (keyIdent) childScope[keyIdent] = key;
1288-
childScope.$index = index;
1289-
childScope.$position = index == 0
1290-
? 'first'
1291-
: (index == collectionLength - 1 ? 'last' : 'middle');
1292-
childScopes.push(childScope);
1303+
}
1304+
1305+
childScope[valueIdent] = collection[key];
1306+
if (keyIdent) childScope[keyIdent] = key;
1307+
childScope.$index = index;
1308+
childScope.$position = index == 0
1309+
? 'first'
1310+
: (index == collectionLength - 1 ? 'last' : 'middle');
1311+
1312+
if (!last) {
12931313
linker(childScope, function(clone){
1294-
clone.attr('ng:repeat-index', index);
1295-
fragment.appendChild(clone[0]);
1296-
// TODO(misko): Temporary hack - maybe think about it - removed after we add fragment after $digest()
1297-
// This causes double $digest for children
1298-
// The first flush will couse a lot of DOM access (initial)
1299-
// Second flush shuld be noop since nothing has change hence no DOM access.
1300-
childScope.$digest();
1301-
childElements[index + 1] = clone;
1314+
cursor.after(clone);
1315+
last = {
1316+
scope: childScope,
1317+
element: (cursor = clone),
1318+
index: index
1319+
};
1320+
nextOrder.push(value, last);
13021321
});
13031322
}
1323+
13041324
index ++;
13051325
}
13061326
}
13071327

1308-
//attach new nodes buffered in doc fragment
1309-
if (addFragmentTo) {
1310-
// TODO(misko): For performance reasons, we should do the addition after all other widgets
1311-
// have run. For this should happend after $digest() is done!
1312-
addFragmentTo.after(jqLite(fragment));
1328+
//shrink children
1329+
for (key in lastOrder) {
1330+
if (lastOrder.hasOwnProperty(key)) {
1331+
array = lastOrder[key];
1332+
while(array.length) {
1333+
value = array.pop();
1334+
value.element.remove();
1335+
value.scope.$destroy();
1336+
}
1337+
}
13131338
}
13141339

1315-
// shrink children
1316-
while(childScopes.length > index) {
1317-
// can not use $destroy(true) since there may be multiple iterators on same parent.
1318-
childScopes.pop().$destroy();
1319-
childElements.pop().remove();
1320-
}
1340+
lastOrder = nextOrder;
13211341
});
13221342
};
13231343
});

0 commit comments

Comments
 (0)