aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/apis.js44
-rw-r--r--src/ng/directive/ngRepeat.js293
-rw-r--r--test/ApiSpecs.js37
-rw-r--r--test/ng/directive/ngClassSpec.js2
-rw-r--r--test/ng/directive/ngRepeatSpec.js322
5 files changed, 346 insertions, 352 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;
+ });
+ };
+ }
+ };
+}];
diff --git a/test/ApiSpecs.js b/test/ApiSpecs.js
index 1e52cf44..12de39d0 100644
--- a/test/ApiSpecs.js
+++ b/test/ApiSpecs.js
@@ -23,42 +23,5 @@ describe('api', function() {
expect(map.get('c')).toBe(undefined);
});
});
-
-
- 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.peek('key')).toEqual('a');
- expect(map[hashKey('key')]).toEqual(['a', 'b']);
- expect(map.shift('key')).toEqual('a');
- expect(map.peek('key')).toEqual('b');
- expect(map[hashKey('key')]).toEqual(['b']);
- expect(map.shift('key')).toEqual('b');
- expect(map.shift('key')).toEqual(undefined);
- expect(map[hashKey('key')]).toEqual(undefined);
- });
-
- it('should support primitive and object keys', function() {
- var obj1 = {},
- obj2 = {};
-
- var map = new HashQueueMap();
- map.push(obj1, 'a1');
- map.push(obj1, 'a2');
- map.push(obj2, 'b');
- map.push(1, 'c');
- map.push(undefined, 'd');
- map.push(null, 'e');
-
- expect(map[hashKey(obj1)]).toEqual(['a1', 'a2']);
- expect(map[hashKey(obj2)]).toEqual(['b']);
- expect(map[hashKey(1)]).toEqual(['c']);
- expect(map[hashKey(undefined)]).toEqual(['d']);
- expect(map[hashKey(null)]).toEqual(['e']);
- });
- });
});
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index 69afef7a..d4bd76fe 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -249,7 +249,7 @@ describe('ngClass', function() {
it('should update ngClassOdd/Even when model is changed by filtering', inject(function($rootScope, $compile) {
element = $compile('<ul>' +
- '<li ng-repeat="i in items" ' +
+ '<li ng-repeat="i in items track by $index" ' +
'ng-class-odd="\'odd\'" ng-class-even="\'even\'"></li>' +
'<ul>')($rootScope);
$rootScope.items = ['a','b','a'];
diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js
index 33e4dcfd..44406d6d 100644
--- a/test/ng/directive/ngRepeatSpec.js
+++ b/test/ng/directive/ngRepeatSpec.js
@@ -1,16 +1,27 @@
'use strict';
describe('ngRepeat', function() {
- var element, $compile, scope;
+ var element, $compile, scope, $exceptionHandler;
- beforeEach(inject(function(_$compile_, $rootScope) {
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ }));
+
+ beforeEach(inject(function(_$compile_, $rootScope, _$exceptionHandler_) {
$compile = _$compile_;
+ $exceptionHandler = _$exceptionHandler_;
scope = $rootScope.$new();
}));
- afterEach(function(){
+ afterEach(function() {
+ if ($exceptionHandler.errors.length) {
+ dump(jasmine.getEnv().currentSpec.getFullName());
+ dump('$exceptionHandler has errors');
+ dump($exceptionHandler.errors);
+ expect($exceptionHandler.errors).toBe([]);
+ }
dealoc(element);
});
@@ -44,141 +55,177 @@ describe('ngRepeat', function() {
});
- it('should iterate over an array of primitives', function() {
+ it('should iterate over on object/map', function() {
element = $compile(
'<ul>' +
- '<li ng-repeat="item in items">{{item}};</li>' +
+ '<li ng-repeat="(key, value) in items">{{key}}:{{value}}|</li>' +
'</ul>')(scope);
-
- Array.prototype.extraProperty = "should be ignored";
- // INIT
- scope.items = [true, true, true];
+ scope.items = {misko:'swe', shyam:'set'};
scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('true;true;true;');
- delete Array.prototype.extraProperty;
+ expect(element.text()).toEqual('misko:swe|shyam:set|');
+ });
- scope.items = [false, true, true];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('false;true;true;');
- scope.items = [false, true, false];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('false;true;false;');
+ describe('track by', function() {
+ it('should track using expression function', function() {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items track by item.id">{{item.name}};</li>' +
+ '</ul>')(scope);
+ scope.items = [{id: 'misko'}, {id: 'igor'}];
+ scope.$digest();
+ var li0 = element.find('li')[0];
+ var li1 = element.find('li')[1];
- scope.items = [true];
- scope.$digest();
- expect(element.find('li').length).toEqual(1);
- expect(element.text()).toEqual('true;');
+ scope.items.push(scope.items.shift());
+ scope.$digest();
+ expect(element.find('li')[0]).toBe(li1);
+ expect(element.find('li')[1]).toBe(li0);
+ });
- scope.items = [true, true, false];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('true;true;false;');
- scope.items = [true, false, false];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('true;false;false;');
+ it('should track using build in $id function', function() {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items track by $id(item)">{{item.name}};</li>' +
+ '</ul>')(scope);
+ scope.items = [{name: 'misko'}, {name: 'igor'}];
+ scope.$digest();
+ var li0 = element.find('li')[0];
+ var li1 = element.find('li')[1];
- // string
- scope.items = ['a', 'a', 'a'];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('a;a;a;');
+ scope.items.push(scope.items.shift());
+ scope.$digest();
+ expect(element.find('li')[0]).toBe(li1);
+ expect(element.find('li')[1]).toBe(li0);
+ });
- scope.items = ['ab', 'a', 'a'];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('ab;a;a;');
- scope.items = ['test'];
- scope.$digest();
- expect(element.find('li').length).toEqual(1);
- expect(element.text()).toEqual('test;');
+ it('should iterate over an array of primitives', function() {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items track by $index">{{item}};</li>' +
+ '</ul>')(scope);
- scope.items = ['same', 'value'];
- scope.$digest();
- expect(element.find('li').length).toEqual(2);
- expect(element.text()).toEqual('same;value;');
+ Array.prototype.extraProperty = "should be ignored";
+ // INIT
+ scope.items = [true, true, true];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('true;true;true;');
+ delete Array.prototype.extraProperty;
- // number
- scope.items = [12, 12, 12];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('12;12;12;');
+ scope.items = [false, true, true];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('false;true;true;');
- scope.items = [53, 12, 27];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('53;12;27;');
+ scope.items = [false, true, false];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('false;true;false;');
- scope.items = [89];
- scope.$digest();
- expect(element.find('li').length).toEqual(1);
- expect(element.text()).toEqual('89;');
+ scope.items = [true];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(1);
+ expect(element.text()).toEqual('true;');
- scope.items = [89, 23];
- scope.$digest();
- expect(element.find('li').length).toEqual(2);
- expect(element.text()).toEqual('89;23;');
- });
+ scope.items = [true, true, false];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('true;true;false;');
+ scope.items = [true, false, false];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('true;false;false;');
- it('should iterate over on object/map', function() {
- element = $compile(
- '<ul>' +
- '<li ng-repeat="(key, value) in items">{{key}}:{{value}}|</li>' +
- '</ul>')(scope);
- scope.items = {misko:'swe', shyam:'set'};
- scope.$digest();
- expect(element.text()).toEqual('misko:swe|shyam:set|');
- });
+ // string
+ scope.items = ['a', 'a', 'a'];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('a;a;a;');
+ scope.items = ['ab', 'a', 'a'];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('ab;a;a;');
- it('should iterate over object with changing primitive property values', function() {
- // test for issue #933
+ scope.items = ['test'];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(1);
+ expect(element.text()).toEqual('test;');
- element = $compile(
- '<ul>' +
- '<li ng-repeat="(key, value) in items">' +
- '{{key}}:{{value}};' +
- '<input type="checkbox" ng-model="items[key]">' +
- '</li>' +
- '</ul>')(scope);
+ scope.items = ['same', 'value'];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(2);
+ expect(element.text()).toEqual('same;value;');
- scope.items = {misko: true, shyam: true, zhenbo:true};
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
+ // number
+ scope.items = [12, 12, 12];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('12;12;12;');
- browserTrigger(element.find('input').eq(0), 'click');
+ scope.items = [53, 12, 27];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('53;12;27;');
- expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
- expect(element.find('input')[0].checked).toBe(false);
- expect(element.find('input')[1].checked).toBe(true);
- expect(element.find('input')[2].checked).toBe(true);
+ scope.items = [89];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(1);
+ expect(element.text()).toEqual('89;');
+
+ scope.items = [89, 23];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(2);
+ expect(element.text()).toEqual('89;23;');
+ });
- browserTrigger(element.find('input').eq(0), 'click');
- expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
- expect(element.find('input')[0].checked).toBe(true);
- expect(element.find('input')[1].checked).toBe(true);
- expect(element.find('input')[2].checked).toBe(true);
- browserTrigger(element.find('input').eq(1), 'click');
- expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;');
- expect(element.find('input')[0].checked).toBe(true);
- expect(element.find('input')[1].checked).toBe(false);
- expect(element.find('input')[2].checked).toBe(true);
+ it('should iterate over object with changing primitive property values', function() {
+ // test for issue #933
- scope.items = {misko: false, shyam: true, zhenbo: true};
- scope.$digest();
- expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
- expect(element.find('input')[0].checked).toBe(false);
- expect(element.find('input')[1].checked).toBe(true);
- expect(element.find('input')[2].checked).toBe(true);
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="(key, value) in items track by $index">' +
+ '{{key}}:{{value}};' +
+ '<input type="checkbox" ng-model="items[key]">' +
+ '</li>' +
+ '</ul>')(scope);
+
+ scope.items = {misko: true, shyam: true, zhenbo:true};
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
+
+ browserTrigger(element.find('input').eq(0), 'click');
+
+ expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
+ expect(element.find('input')[0].checked).toBe(false);
+ expect(element.find('input')[1].checked).toBe(true);
+ expect(element.find('input')[2].checked).toBe(true);
+
+ browserTrigger(element.find('input').eq(0), 'click');
+ expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
+ expect(element.find('input')[0].checked).toBe(true);
+ expect(element.find('input')[1].checked).toBe(true);
+ expect(element.find('input')[2].checked).toBe(true);
+
+ browserTrigger(element.find('input').eq(1), 'click');
+ expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;');
+ expect(element.find('input')[0].checked).toBe(true);
+ expect(element.find('input')[1].checked).toBe(false);
+ expect(element.find('input')[2].checked).toBe(true);
+
+ scope.items = {misko: false, shyam: true, zhenbo: true};
+ scope.$digest();
+ expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
+ expect(element.find('input')[0].checked).toBe(false);
+ expect(element.find('input')[1].checked).toBe(true);
+ expect(element.find('input')[2].checked).toBe(true);
+ });
});
@@ -199,19 +246,18 @@ describe('ngRepeat', function() {
it('should error on wrong parsing of ngRepeat', function() {
- expect(function() {
- element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
- $compile(element)(scope);
- }).toThrow("Expected ngRepeat in form of '_item_ in _collection_' but got 'i dont parse'.");
+ element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
+ $compile(element)(scope);
+ expect($exceptionHandler.errors.shift()[0].message).
+ toBe("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'.");
});
it("should throw error when left-hand-side of ngRepeat can't be parsed", function() {
- expect(function() {
element = jqLite('<ul><li ng-repeat="i dont parse in foo"></li></ul>');
$compile(element)(scope);
- }).toThrow("'item' in 'item in collection' should be identifier or (key, value) but got " +
- "'i dont parse'.");
+ expect($exceptionHandler.errors.shift()[0].message).
+ toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'.");
});
@@ -311,7 +357,7 @@ describe('ngRepeat', function() {
it('should ignore $ and $$ properties', function() {
element = $compile('<ul><li ng-repeat="i in items">{{i}}|</li></ul>')(scope);
scope.items = ['a', 'b', 'c'];
- scope.items.$$hashkey = 'xxx';
+ scope.items.$$hashKey = 'xxx';
scope.items.$root = 'yyy';
scope.$digest();
@@ -393,43 +439,23 @@ describe('ngRepeat', function() {
});
- it('should support duplicates', function() {
- scope.items = [a, a, b, c];
- scope.$digest();
- var newElements = element.find('li');
- expect(newElements[0]).toEqual(lis[0]);
- expect(newElements[1]).not.toEqual(lis[0]);
- expect(newElements[2]).toEqual(lis[1]);
- expect(newElements[3]).toEqual(lis[2]);
-
- lis = newElements;
+ it('should throw error on duplicates and recover', function() {
+ scope.items = [a, a, a];
scope.$digest();
- newElements = element.find('li');
- expect(newElements[0]).toEqual(lis[0]);
- expect(newElements[1]).toEqual(lis[1]);
- expect(newElements[2]).toEqual(lis[2]);
- expect(newElements[3]).toEqual(lis[3]);
+ expect($exceptionHandler.errors.shift().message).
+ toEqual('Duplicates in a repeater are not allowed. Repeater: item in items');
+ // recover
+ scope.items = [a];
scope.$digest();
- newElements = element.find('li');
+ var newElements = element.find('li');
+ expect(newElements.length).toEqual(1);
expect(newElements[0]).toEqual(lis[0]);
- expect(newElements[1]).toEqual(lis[1]);
- expect(newElements[2]).toEqual(lis[2]);
- expect(newElements[3]).toEqual(lis[3]);
- });
-
- it('should remove last item when one duplicate instance is removed', function() {
- scope.items = [a, a, a];
- scope.$digest();
- lis = element.find('li');
-
- scope.items = [a, a];
+ scope.items = [];
scope.$digest();
var newElements = element.find('li');
- expect(newElements.length).toEqual(2);
- expect(newElements[0]).toEqual(lis[0]);
- expect(newElements[1]).toEqual(lis[1]);
+ expect(newElements.length).toEqual(0);
});