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

feat(ngRepeat): add support for hash for item in items #2216

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 196 additions & 116 deletions src/ng/directive/ngRepeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@
*
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
*
* * `hash_expression from variable in expression` – You can also provide an optional hashing function
* which can be used to associate the objects in the collection with the DOM elements. If no hashing function
* is specified the ng-repeat associates elements by position in the collection. items. It is an error to have
* more then one item hash resolve to the same key. (This would mean that two distinct objects are mapped to
* the same DOM element, which is not possible.)
*
* For example: `item in items` is equivalent to `$index from item in items'. This implies that the DOM elements
* will be associated by item position in array.
*
* For example: `$hash(item) from item in items`. A built in `$hash()` function can be used to assign a unique
* `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
* with the corresponding item in the array by identity. Moving the same object in array would move the DOM
* element in the same way in the DOM.
*
* For example: `item.id from item it items` Is a typical pattern when the items come from the database. In this
* case the object identity does not matter. Two objects are considered the equivalent as long as their `id`
* is same.
*
* @example
* This example initializes the scope to a list of names and
* then uses `ngRepeat` to display every person:
Expand All @@ -57,133 +75,195 @@
</doc:scenario>
</doc:example>
*/
var ngRepeatDirective = ngDirective({
transclude: 'element',
priority: 1000,
terminal: true,
compile: function(element, attr, linker) {
return function(scope, iterStartElement, attr){
var expression = attr.ngRepeat;
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
lhs, rhs, valueIdent, keyIdent;
if (! match) {
throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" +
expression + "'.");
}
lhs = match[1];
rhs = match[2];
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
if (!match) {
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
lhs + "'.");
}
valueIdent = match[3] || match[1];
keyIdent = match[2];

// 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();

scope.$watch(function ngRepeatWatch(scope){
var index, length,
collection = scope.$eval(rhs),
cursor = iterStartElement, // current position of the node
// Same as lastOrder but it has the current state. It will become the
// lastOrder on the next iteration.
nextOrder = new HashQueueMap(),
arrayLength,
childScope,
key, value, // key/value of iteration
array,
last; // last object information {scope, element, index}



if (!isArray(collection)) {
// if object, extract keys, sort them and use to determine order of iteration over obj props
array = [];
for(key in collection) {
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
array.push(key);
}
}
array.sort();
var ngRepeatDirective = ['$parse', function($parse) {
return {
transclude: 'element',
priority: 1000,
terminal: true,
compile: function(element, attr, linker) {
return function(scope, iterStartElement, attr){
var expression = attr.ngRepeat;
var match = expression.match(/^((.+)\s+from)?\s*(.+)\s+in\s+(.*)\s*$/),
hashExp, hashExpFn, hashFn, lhs, rhs, valueIdent, keyIdent,
hashFnLocals = {$hash: hashKey};

if (! match) {
throw Error("Expected ngRepeat in form of '(_hash_ from) _item_ in _collection_' but got '" +
expression + "'.");
}

hashExp = match[2];
lhs = match[3];
rhs = match[4];

if (hashExp) {
hashExpFn = $parse(hashExp);
hashFn = function(key, value) {
if (keyIdent) hashFnLocals[keyIdent] = key;
hashFnLocals[valueIdent] = value;
return hashExpFn(scope, hashFnLocals);
};
} else {
array = collection || [];
hashFn = function(key, value) {
return key;
}
}

match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
if (!match) {
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
lhs + "'.");
}
valueIdent = match[3] || match[1];
keyIdent = match[2];

arrayLength = array.length;
// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is objects with following properties.
// - scope: bound scope
// - element: previous element.
// - index: position
var lastOrder = new HashMap();

// we are not using forEach for perf reasons (trying to avoid #call)
for (index = 0, length = array.length; index < length; index++) {
key = (collection === array) ? index : array[index];
value = collection[key];
// Store the list of item orders. Need so that we can compute moves. When an item at position
// #2 gets removed then item ot old position #3 becomes #2, but that is not considered a move
var blockHead = {
element: iterStartElement,
next: null
},
blockRemove = function(block) {
var right = block.next;
var left = block.prev;

last = lastOrder.shift(value);
if (right) right.prev = left;
left.next = right;
},
blockInsertAfter = function(afterBlock, newBlock) {
var right = afterBlock.next;

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);
newBlock.next = right;
newBlock.prev = afterBlock;

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;
}
afterBlock.next = newBlock;
if (right) right.prev = newBlock;
},
iterStartBlock = {element: iterStartElement, next: null, prev: null};

//watch props
scope.$watchProps(rhs, function ngRepeatWatch(collection){
var index, length,
cursor = iterStartElement, // current position of the node
// Same as lastOrder but it has the current state. It will become the
// lastOrder on the next iteration.
nextOrder = new HashMap(), //use HashMap
arrayLength,
childScope,
key, value, // key/value of iteration
hashCode,
collectionKeys,
block, // last object information {scope, element, index}
blocks = [],
lastBlock = iterStartBlock;


if (isArray(collection)) {
collectionKeys = collection || [];
} else {
// new item which we don't know about
childScope = scope.$new();
// if object, extract keys, sort them and use to determine order of iteration over obj props
collectionKeys = [];
for (key in collection) {
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
collectionKeys.push(key);
}
}
collectionKeys.sort();
}

childScope[valueIdent] = value;
if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index;

childScope.$first = (index === 0);
childScope.$last = (index === (arrayLength - 1));
childScope.$middle = !(childScope.$first || childScope.$last);

if (!last) {
linker(childScope, function(clone){
cursor.after(clone);
last = {
scope: childScope,
element: (cursor = clone),
index: index
};
nextOrder.push(value, last);
});
}
}
arrayLength = collectionKeys.length;

//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();
// locate existing items
length = blocks.length = collectionKeys.length;
for(index = 0; index < length; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
hashCode = hashFn(key, value);
if((block = lastOrder.remove(hashCode))) {
nextOrder.put(hashCode, block);
blocks[index] = block;
} else if (nextOrder.get(hashCode)) {
// restore lastOrder
forEach(blocks, function(block) {
if (block && block.element) lastOrder.put(block.hash, block);
});
// This is a duplicate and we need to throw an error
throw new Error('Duplicate hashes in the repeater are not allowed.');
} else {
// new never before seen block
blocks[index] = { hash: hashCode };
}
}

// remove existing items
for (key in lastOrder) {
if (lastOrder.hasOwnProperty(key)) {
block = lastOrder[key];
block.element.remove();
block.scope.$destroy();
blockRemove(block);
}
}
}

lastOrder = nextOrder;
});
};
}
});
// we are not using forEach for perf reasons (trying to avoid #call)
for (index = 0, length = collectionKeys.length; index < length; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
block = blocks[index];

if (block.element) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
childScope = block.scope;

if (block.element == cursor) {
// do nothing
cursor = block.element;
} else {
// existing item which got moved
blockRemove(block);
blockInsertAfter(lastBlock, block);
cursor.after(block.element);
cursor = block.element;
}
} else {
// new item which we don't know about
childScope = scope.$new();
}

childScope[valueIdent] = value;
if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index;
childScope.$first = (index === 0);
childScope.$last = (index === (arrayLength - 1));
childScope.$middle = !(childScope.$first || childScope.$last);

if (!block.element) {
linker(childScope, function(clone){
cursor.after(clone);
cursor = clone;
block.scope = childScope;
block.element = clone;
blockInsertAfter(lastBlock, block);
nextOrder.put(block.hash, block);
});
}
}
if (block) {
block.next = null;
} else {
iterStartBlock.next = null;
}
lastOrder = nextOrder;
});
};
}
};
}];
Loading