diff options
| -rw-r--r-- | src/apis.js | 44 | ||||
| -rw-r--r-- | src/ng/directive/ngRepeat.js | 293 | ||||
| -rw-r--r-- | test/ApiSpecs.js | 37 | ||||
| -rw-r--r-- | test/ng/directive/ngClassSpec.js | 2 | ||||
| -rw-r--r-- | test/ng/directive/ngRepeatSpec.js | 322 |
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); }); |
