diff --git a/CHANGELOG.md b/CHANGELOG.md
index 856731b28b99..cd5fb2b50db6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
@@ -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
diff --git a/src/apis.js b/src/apis.js
index 7aa9f0c2bfae..bec54b8e350f 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -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;
@@ -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;
},
/**
@@ -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){
diff --git a/src/widgets.js b/src/widgets.js
index bd7c3d7f8847..1047c3ce29cf 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -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:
*
@@ -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;
});
};
});
diff --git a/test/ApiSpecs.js b/test/ApiSpecs.js
index ef25bb413149..9683a7b769e4 100644
--- a/test/ApiSpecs.js
+++ b/test/ApiSpecs.js
@@ -1,26 +1,39 @@
'use strict';
-describe('api', function(){
+describe('api', function() {
- describe('HashMap', function(){
- it('should do basic crud', function(){
+ describe('HashMap', function() {
+ it('should do basic crud', function() {
var map = new HashMap();
var key = {};
var value1 = {};
var value2 = {};
- expect(map.put(key, value1)).toEqual(undefined);
- expect(map.put(key, value2)).toEqual(value1);
- expect(map.get(key)).toEqual(value2);
- expect(map.get({})).toEqual(undefined);
- expect(map.remove(key)).toEqual(value2);
- expect(map.get(key)).toEqual(undefined);
+ map.put(key, value1);
+ map.put(key, value2);
+ expect(map.get(key)).toBe(value2);
+ expect(map.get({})).toBe(undefined);
+ expect(map.remove(key)).toBe(value2);
+ expect(map.get(key)).toBe(undefined);
});
});
- describe('Object', function(){
+ describe('HashQueueMap', function() {
+ it('should do basic crud with collections', function() {
+ var map = new HashQueueMap();
+ map.push('key', 'a');
+ map.push('key', 'b');
+ expect(map[hashKey('key')]).toEqual(['a', 'b']);
+ expect(map.shift('key')).toEqual('a');
+ expect(map.shift('key')).toEqual('b');
+ expect(map.shift('key')).toEqual(undefined);
+ expect(map[hashKey('key')]).toEqual(undefined);
+ });
+ });
+
- it('should return type of', function(){
+ describe('Object', function() {
+ it('should return type of', function() {
assertEquals("undefined", angular.Object.typeOf(undefined));
assertEquals("null", angular.Object.typeOf(null));
assertEquals("object", angular.Collection.typeOf({}));
@@ -28,46 +41,45 @@ describe('api', function(){
assertEquals("string", angular.Object.typeOf(""));
assertEquals("date", angular.Object.typeOf(new Date()));
assertEquals("element", angular.Object.typeOf(document.body));
- assertEquals('function', angular.Object.typeOf(function(){}));
+ assertEquals('function', angular.Object.typeOf(function() {}));
});
- it('should extend object', function(){
+ it('should extend object', function() {
assertEquals({a:1, b:2}, angular.Object.extend({a:1}, {b:2}));
});
-
});
- it('should return size', function(){
+ it('should return size', function() {
assertEquals(0, angular.Collection.size({}));
assertEquals(1, angular.Collection.size({a:"b"}));
assertEquals(0, angular.Object.size({}));
assertEquals(1, angular.Array.size([0]));
});
- describe('Array', function(){
- describe('sum', function(){
+ describe('Array', function() {
- it('should sum', function(){
+ describe('sum', function() {
+ it('should sum', function() {
assertEquals(3, angular.Array.sum([{a:"1"}, {a:"2"}], 'a'));
});
- it('should sum containing NaN', function(){
+ it('should sum containing NaN', function() {
assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], 'a'));
- assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], function($){return $.a;}));
+ assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], function($) {return $.a;}));
});
-
});
- it('should find indexOf', function(){
+
+ it('should find indexOf', function() {
assertEquals(angular.Array.indexOf(['a'], 'a'), 0);
assertEquals(angular.Array.indexOf(['a', 'b'], 'a'), 0);
assertEquals(angular.Array.indexOf(['b', 'a'], 'a'), 1);
assertEquals(angular.Array.indexOf(['b', 'b'],'x'), -1);
});
- it('should remove item from array', function(){
+ it('should remove item from array', function() {
var items = ['a', 'b', 'c'];
assertEquals(angular.Array.remove(items, 'q'), 'q');
assertEquals(items.length, 3);
@@ -85,8 +97,8 @@ describe('api', function(){
assertEquals(items.length, 0);
});
- describe('filter', function(){
+ describe('filter', function() {
it('should filter by string', function() {
var items = ["MIsKO", {name:"shyam"}, ["adam"], 1234];
assertEquals(4, angular.Array.filter(items, "").length);
@@ -113,7 +125,7 @@ describe('api', function(){
assertEquals(0, angular.Array.filter(items, "misko").length);
});
- it('should filter on specific property', function(){
+ it('should filter on specific property', function() {
var items = [{ignore:"a", name:"a"}, {ignore:"a", name:"abc"}];
assertEquals(2, angular.Array.filter(items, {}).length);
@@ -123,12 +135,12 @@ describe('api', function(){
assertEquals("abc", angular.Array.filter(items, {name:'b'})[0].name);
});
- it('should take function as predicate', function(){
+ it('should take function as predicate', function() {
var items = [{name:"a"}, {name:"abc", done:true}];
- assertEquals(1, angular.Array.filter(items, function(i){return i.done;}).length);
+ assertEquals(1, angular.Array.filter(items, function(i) {return i.done;}).length);
});
- it('should take object as perdicate', function(){
+ it('should take object as perdicate', function() {
var items = [{first:"misko", last:"hevery"},
{first:"adam", last:"abrons"}];
@@ -139,7 +151,7 @@ describe('api', function(){
assertEquals(items[0], angular.Array.filter(items, {first:'misko', last:'hevery'})[0]);
});
- it('should support negation operator', function(){
+ it('should support negation operator', function() {
var items = ["misko", "adam"];
assertEquals(1, angular.Array.filter(items, '!isk').length);
@@ -198,12 +210,12 @@ describe('api', function(){
});
- it('add', function(){
+ it('add', function() {
var add = angular.Array.add;
assertJsonEquals([{}, "a"], add(add([]),"a"));
});
- it('count', function(){
+ it('count', function() {
var array = [{name:'a'},{name:'b'},{name:''}];
var obj = {};
@@ -212,24 +224,25 @@ describe('api', function(){
assertEquals(1, angular.Array.count(array, 'name=="a"'));
});
- describe('orderBy', function(){
+
+ describe('orderBy', function() {
var orderBy;
- beforeEach(function(){
+ beforeEach(function() {
orderBy = angular.Array.orderBy;
});
- it('should return same array if predicate is falsy', function(){
+ it('should return same array if predicate is falsy', function() {
var array = [1, 2, 3];
expect(orderBy(array)).toBe(array);
});
- it('shouldSortArrayInReverse', function(){
+ it('shouldSortArrayInReverse', function() {
assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', true));
assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', "T"));
assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', "reverse"));
});
- it('should sort array by predicate', function(){
+ it('should sort array by predicate', function() {
assertJsonEquals([{a:2, b:1},{a:15, b:1}],
angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['a', 'b']));
assertJsonEquals([{a:2, b:1},{a:15, b:1}],
@@ -238,11 +251,11 @@ describe('api', function(){
angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['+b', '-a']));
});
- it('should use function', function(){
+ it('should use function', function() {
expect(
orderBy(
[{a:15, b:1},{a:2, b:1}],
- function(value){ return value.a; })).
+ function(value) { return value.a; })).
toEqual([{a:2, b:1},{a:15, b:1}]);
});
@@ -250,9 +263,9 @@ describe('api', function(){
});
- describe('string', function(){
- it('should quote', function(){
+ describe('string', function() {
+ it('should quote', function() {
assertEquals(angular.String.quote('a'), '"a"');
assertEquals(angular.String.quote('\\'), '"\\\\"');
assertEquals(angular.String.quote("'a'"), '"\'a\'"');
@@ -260,22 +273,22 @@ describe('api', function(){
assertEquals(angular.String.quote('\n\f\r\t'), '"\\n\\f\\r\\t"');
});
- it('should quote slashes', function(){
+ it('should quote slashes', function() {
assertEquals('"7\\\\\\\"7"', angular.String.quote("7\\\"7"));
});
- it('should quote unicode', function(){
+ it('should quote unicode', function() {
assertEquals('"abc\\u00a0def"', angular.String.quoteUnicode('abc\u00A0def'));
});
- it('should read/write to date', function(){
+ it('should read/write to date', function() {
var date = new Date("Sep 10 2003 13:02:03 GMT");
assertEquals("date", angular.Object.typeOf(date));
assertEquals("2003-09-10T13:02:03.000Z", angular.Date.toString(date));
assertEquals(date.getTime(), angular.String.toDate(angular.Date.toString(date)).getTime());
});
- it('should convert to date', function(){
+ it('should convert to date', function() {
//full ISO8061
expect(angular.String.toDate("2003-09-10T13:02:03.000Z")).
toEqual(new Date("Sep 10 2003 13:02:03 GMT"));
@@ -297,14 +310,12 @@ describe('api', function(){
toEqual(new Date("Sep 10 2003 00:00:00 GMT"));
});
- it('should parse date', function(){
+ it('should parse date', function() {
var date = angular.String.toDate("2003-09-10T13:02:03.000Z");
assertEquals("date", angular.Object.typeOf(date));
assertEquals("2003-09-10T13:02:03.000Z", angular.Date.toString(date));
assertEquals("str", angular.String.toDate("str"));
});
-
});
-
});
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index 68513f625b34..93f23eefdfa4 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -194,25 +194,25 @@ describe('Binder', function(){
scope.$apply();
assertEquals('