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

Commit

Permalink
feat(ng:repeat) collection items and DOM elements affinity / stability
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery authored and IgorMinar committed Oct 11, 2011
1 parent e134a83 commit 75f11f1
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 227 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
- If Angular is being used with jQuery older than 1.6, some features might not work properly. Please
upgrade to jQuery version 1.6.4.


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


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

Expand Down
58 changes: 44 additions & 14 deletions src/apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -840,20 +840,22 @@ var angularFunction = {
* Hash of a:
* string is string
* number is number as string
* object is either call $hashKey function on object or assign unique hashKey id.
* object is either result of calling $$hashKey function on the object or uniquely generated id,
* that is also assigned to the $$hashKey property of the object.
*
* @param obj
* @returns {String} hash string such that the same input will have the same hash string
* @returns {String} hash string such that the same input will have the same hash string.
* The resulting string key is in 'type:hashKey' format.
*/
function hashKey(obj) {
var objType = typeof obj;
var key = obj;
if (objType == 'object') {
if (typeof (key = obj.$hashKey) == 'function') {
if (typeof (key = obj.$$hashKey) == 'function') {
// must invoke on object to keep the right this
key = obj.$hashKey();
key = obj.$$hashKey();
} else if (key === undefined) {
key = obj.$hashKey = nextUid();
key = obj.$$hashKey = nextUid();
}
}
return objType + ':' + key;
Expand All @@ -868,13 +870,9 @@ HashMap.prototype = {
* Store key value pair
* @param key key to store can be any type
* @param value value to store can be any type
* @returns old value if any
*/
put: function(key, value) {
var _key = hashKey(key);
var oldValue = this[_key];
this[_key] = value;
return oldValue;
this[hashKey(key)] = value;
},

/**
Expand All @@ -888,16 +886,48 @@ HashMap.prototype = {
/**
* Remove the key/value pair
* @param key
* @returns value associated with key before it was removed
*/
remove: function(key) {
var _key = hashKey(key);
var value = this[_key];
delete this[_key];
var value = this[key = hashKey(key)];
delete this[key];
return value;
}
};

/**
* A map where multiple values can be added to the same key such that the form a queue.
* @returns {HashQueueMap}
*/
function HashQueueMap(){}
HashQueueMap.prototype = {
/**
* Same as array push, but using an array as the value for the hash
*/
push: function(key, value) {
var array = this[key = hashKey(key)];
if (!array) {
this[key] = [value];
} else {
array.push(value);
}
},

/**
* Same as array shift, but using an array as the value for the hash
*/
shift: function(key) {
var array = this[key = hashKey(key)];
if (array) {
if (array.length == 1) {
delete this[key];
return array[0];
} else {
return array.shift();
}
}
}
};

function defineApi(dst, chain){
angular[dst] = angular[dst] || {};
forEach(chain, function(parent){
Expand Down
112 changes: 66 additions & 46 deletions src/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -1182,10 +1182,9 @@ angularWidget('a', function() {
* @name angular.widget.@ng:repeat
*
* @description
* The `ng:repeat` widget instantiates a template once per item from a collection. The collection is
* enumerated with the `ng:repeat-index` attribute, starting from 0. Each template instance gets
* its own scope, where the given loop variable is set to the current collection item, and `$index`
* is set to the item index or key.
* The `ng:repeat` widget instantiates a template once per item from a collection. Each template
* instance gets its own scope, where the given loop variable is set to the current collection item,
* and `$index` is set to the item index or key.
*
* Special properties are exposed on the local scope of each template instance, including:
*
Expand Down Expand Up @@ -1256,68 +1255,89 @@ angularWidget('@ng:repeat', function(expression, element){
valueIdent = match[3] || match[1];
keyIdent = match[2];

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

for (key in collection) {
if (collection.hasOwnProperty(key)) {
if (index < childCount) {
// reuse existing child
childScope = childScopes[index];
childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key;
childScope.$position = index == 0
? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle');
childScope.$eval();
last = lastOrder.shift(value = collection[key]);
if (last) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
childScope = last.scope;
nextOrder.push(value, last);

if (index === last.index) {
// do nothing
cursor = last.element;
} else {
// existing item which got moved
last.index = index;
// This may be a noop, if the element is next, but I don't know of a good way to
// figure this out, since it would require extra DOM access, so let's just hope that
// the browsers realizes that it is noop, and treats it as such.
cursor.after(last.element);
cursor = last.element;
}
} else {
// grow children
// new item which we don't know about
childScope = parentScope.$new();
childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index;
childScope.$position = index == 0
? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle');
childScopes.push(childScope);
}

childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index;
childScope.$position = index == 0
? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle');

if (!last) {
linker(childScope, function(clone){
clone.attr('ng:repeat-index', index);
fragment.appendChild(clone[0]);
// TODO(misko): Temporary hack - maybe think about it - removed after we add fragment after $digest()
// This causes double $digest for children
// The first flush will couse a lot of DOM access (initial)
// Second flush shuld be noop since nothing has change hence no DOM access.
childScope.$digest();
childElements[index + 1] = clone;
cursor.after(clone);
last = {
scope: childScope,
element: (cursor = clone),
index: index
};
nextOrder.push(value, last);
});
}

index ++;
}
}

//attach new nodes buffered in doc fragment
if (addFragmentTo) {
// TODO(misko): For performance reasons, we should do the addition after all other widgets
// have run. For this should happend after $digest() is done!
addFragmentTo.after(jqLite(fragment));
//shrink children
for (key in lastOrder) {
if (lastOrder.hasOwnProperty(key)) {
array = lastOrder[key];
while(array.length) {
value = array.pop();
value.element.remove();
value.scope.$destroy();
}
}
}

// shrink children
while(childScopes.length > index) {
// can not use $destroy(true) since there may be multiple iterators on same parent.
childScopes.pop().$destroy();
childElements.pop().remove();
}
lastOrder = nextOrder;
});
};
});
Expand Down
Loading

0 comments on commit 75f11f1

Please sign in to comment.