aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMisko Hevery2013-03-19 22:27:27 -0700
committerMisko Hevery2013-03-29 23:01:52 -0700
commit61f2767ce65562257599649d9eaf9da08f321655 (patch)
treecf9c6809bbee62d19e04961f53d5c89bab9dd663 /src
parent5eb968553a1130461ab8704535691e00eb154ac2 (diff)
downloadangular.js-61f2767ce65562257599649d9eaf9da08f321655.tar.bz2
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.)
Diffstat (limited to 'src')
-rw-r--r--src/apis.js44
-rw-r--r--src/ng/directive/ngRepeat.js293
2 files changed, 171 insertions, 166 deletions
diff --git a/src/apis.js b/src/apis.js
index 0e94e2a5..c5d2b3d3 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -65,47 +65,3 @@ HashMap.prototype = {
return value;
}
};
-
-/**
- * A map where multiple values can be added to the same key such that they 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();
- }
- }
- },
-
- /**
- * return the first item without deleting it
- */
- peek: function(key) {
- var array = this[hashKey(key)];
- if (array) {
- return array[0];
- }
- }
-};
diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js
index ea96d0ad..a00bc9e4 100644
--- a/src/ng/directive/ngRepeat.js
+++ b/src/ng/directive/ngRepeat.js
@@ -20,7 +20,7 @@
* @element ANY
* @scope
* @priority 1000
- * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two
+ * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
* formats are currently supported:
*
* * `variable in expression` – where variable is the user defined loop variable and `expression`
@@ -33,6 +33,24 @@
*
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
*
+ * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
+ * which can be used to associate the objects in the collection with the DOM elements. If no tractking function
+ * is specified the ng-repeat associates elements by identity in the collection. It is an error to have
+ * more then one tractking function to 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 `item in items track by $id(item)'. This implies that the DOM elements
+ * will be associated by item identity in the array.
+ *
+ * For example: `item in items track by $id(item)`. A built in `$id()` 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 ian the DOM.
+ *
+ * For example: `item in items track by item.id` Is a typical pattern when the items come from the database. In this
+ * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
+ * property is same.
+ *
* @example
* This example initializes the scope to a list of names and
* then uses `ngRepeat` to display every person:
@@ -57,133 +75,164 @@
</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(),
- arrayBound,
- 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();
- } else {
- array = collection || [];
+var ngRepeatDirective = ['$parse', function($parse) {
+ return {
+ transclude: 'element',
+ priority: 1000,
+ terminal: true,
+ compile: function(element, attr, linker) {
+ return function($scope, $element, $attr){
+ var expression = $attr.ngRepeat;
+ var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
+ trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
+ hashFnLocals = {$id: hashKey};
+
+ if (!match) {
+ throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
+ expression + "'.");
}
- arrayBound = array.length-1;
-
- // 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];
-
- last = lastOrder.shift(value);
-
- 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;
- }
+ lhs = match[1];
+ rhs = match[2];
+ trackByExp = match[4];
+
+ if (trackByExp) {
+ hashExpFn = $parse(trackByExp);
+ trackByIdFn = function(key, value, index) {
+ // assign key, value, and $index to the locals so that they can be used in hash functions
+ if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
+ hashFnLocals[valueIdentifier] = value;
+ hashFnLocals.$index = index;
+ return hashExpFn($scope, hashFnLocals);
+ };
+ } else {
+ trackByIdFn = function(key, value) {
+ return hashKey(value);
+ }
+ }
+
+ 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 + "'.");
+ }
+ valueIdentifier = match[3] || match[1];
+ keyIdentifier = 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 objects with following properties.
+ // - scope: bound scope
+ // - element: previous element.
+ // - index: position
+ var lastBlockMap = {};
+
+ //watch props
+ $scope.$watchCollection(rhs, function ngRepeatAction(collection){
+ var index, length,
+ cursor = $element, // current position of the node
+ // Same as lastBlockMap but it has the current state. It will become the
+ // lastBlockMap on the next iteration.
+ nextBlockMap = {},
+ arrayLength,
+ childScope,
+ key, value, // key/value of iteration
+ trackById,
+ collectionKeys,
+ block, // last object information {scope, element, id}
+ nextBlockOrder = [];
+
+
+ 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 === arrayBound);
- 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;
+
+ // locate existing items
+ length = nextBlockOrder.length = collectionKeys.length;
+ for(index = 0; index < length; index++) {
+ key = (collection === collectionKeys) ? index : collectionKeys[index];
+ value = collection[key];
+ trackById = trackByIdFn(key, value, index);
+ if((block = lastBlockMap[trackById])) {
+ delete lastBlockMap[trackById];
+ nextBlockMap[trackById] = block;
+ nextBlockOrder[index] = block;
+ } else if (nextBlockMap.hasOwnProperty(trackById)) {
+ // restore lastBlockMap
+ forEach(nextBlockOrder, function(block) {
+ if (block && block.element) lastBlockMap[block.id] = block;
+ });
+ // This is a duplicate and we need to throw an error
+ throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression);
+ } else {
+ // new never before seen block
+ nextBlockOrder[index] = { id: trackById };
+ }
+ }
+
+ // remove existing items
+ for (key in lastBlockMap) {
+ if (lastBlockMap.hasOwnProperty(key)) {
+ block = lastBlockMap[key];
+ block.element.remove();
+ block.scope.$destroy();
+ }
}
- }
- //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();
+ // 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 = nextBlockOrder[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
+ cursor.after(block.element);
+ cursor = block.element;
+ }
+ } else {
+ // new item which we don't know about
+ childScope = $scope.$new();
}
- }
- }
- lastOrder = nextOrder;
- });
- };
- }
-});
+ childScope[valueIdentifier] = value;
+ if (keyIdentifier) childScope[keyIdentifier] = 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;
+ nextBlockMap[block.id] = block;
+ });
+ }
+ }
+ lastBlockMap = nextBlockMap;
+ });
+ };
+ }
+ };
+}];