diff options
| -rw-r--r-- | src/ng/directive/ngRepeat.js | 32 | ||||
| -rw-r--r-- | test/BinderSpec.js | 27 | ||||
| -rw-r--r-- | test/helpers/testabilityPatch.js | 11 | ||||
| -rw-r--r-- | test/ng/directive/ngClassSpec.js | 10 | ||||
| -rw-r--r-- | test/ng/directive/ngRepeatSpec.js | 163 |
5 files changed, 212 insertions, 31 deletions
diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 78b054ff..bce76411 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -279,7 +279,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { trackByIdFn, collectionKeys, block, // last object information {scope, element, id} - nextBlockOrder = []; + nextBlockOrder = [], + elementsToRemove; if (isArrayLike(collection)) { @@ -331,8 +332,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; - $animate.leave(block.elements); - forEach(block.elements, function(element) { element[NG_REMOVED] = true}); + elementsToRemove = getBlockElements(block); + $animate.leave(elementsToRemove); + forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); block.scope.$destroy(); } } @@ -342,6 +344,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; + if (nextBlockOrder[index - 1]) previousNode = nextBlockOrder[index - 1].endNode; if (block.startNode) { // if we have already seen this object, then we need to reuse the @@ -357,7 +360,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // do nothing } else { // existing item which got moved - $animate.move(block.elements, null, jqLite(previousNode)); + $animate.move(getBlockElements(block), null, jqLite(previousNode)); } previousNode = block.endNode; } else { @@ -375,11 +378,11 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (!block.startNode) { linker(childScope, function(clone) { + clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone; block.scope = childScope; - block.startNode = clone[0]; - block.elements = clone; + block.startNode = previousNode && previousNode.endNode ? previousNode.endNode : clone[0]; block.endNode = clone[clone.length - 1]; nextBlockMap[block.id] = block; }); @@ -390,5 +393,22 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { }; } }; + + function getBlockElements(block) { + if (block.startNode === block.endNode) { + return jqLite(block.startNode); + } + + var element = block.startNode; + var elements = [element]; + + do { + element = element.nextSibling; + if (!element) break; + elements.push(element); + } while (element !== block.endNode); + + return jqLite(elements); + } }]; diff --git a/test/BinderSpec.js b/test/BinderSpec.js index 3c204b64..b553c68d 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -96,7 +96,9 @@ describe('Binder', function() { '<ul>' + '<!-- ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">A</li>' + + '<!-- end ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">B</li>' + + '<!-- end ngRepeat: item in model.items -->' + '</ul>'); items.unshift({a: 'C'}); @@ -105,8 +107,11 @@ describe('Binder', function() { '<ul>' + '<!-- ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">C</li>' + + '<!-- end ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">A</li>' + + '<!-- end ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">B</li>' + + '<!-- end ngRepeat: item in model.items -->' + '</ul>'); items.shift(); @@ -115,7 +120,9 @@ describe('Binder', function() { '<ul>' + '<!-- ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">A</li>' + + '<!-- end ngRepeat: item in model.items -->' + '<li ng-bind="item.a" ng-repeat="item in model.items">B</li>' + + '<!-- end ngRepeat: item in model.items -->' + '</ul>'); items.shift(); @@ -134,6 +141,7 @@ describe('Binder', function() { '<ul>' + '<!-- ngRepeat: item in model.items -->' + '<li ng-repeat="item in model.items"><span ng-bind="item.a">A</span></li>' + + '<!-- end ngRepeat: item in model.items -->' + '</ul>'); })); @@ -148,15 +156,15 @@ describe('Binder', function() { $rootScope.items = items; $rootScope.$apply(); - expect(element[0].childNodes.length - 1).toEqual(0); + expect(element[0].childNodes.length).toEqual(1); items.name = 'misko'; $rootScope.$apply(); - expect(element[0].childNodes.length - 1).toEqual(1); + expect(element[0].childNodes.length).toEqual(3); delete items.name; $rootScope.$apply(); - expect(element[0].childNodes.length - 1).toEqual(0); + expect(element[0].childNodes.length).toEqual(1); })); it('IfTextBindingThrowsErrorDecorateTheSpan', function() { @@ -223,13 +231,19 @@ describe('Binder', function() { '<div name="a" ng-repeat="m in model">'+ '<!-- ngRepeat: i in m.item -->' + '<ul name="a1" ng-repeat="i in m.item"></ul>'+ + '<!-- end ngRepeat: i in m.item -->' + '<ul name="a2" ng-repeat="i in m.item"></ul>'+ + '<!-- end ngRepeat: i in m.item -->' + '</div>'+ + '<!-- end ngRepeat: m in model -->' + '<div name="b" ng-repeat="m in model">'+ '<!-- ngRepeat: i in m.item -->' + '<ul name="b1" ng-repeat="i in m.item"></ul>'+ + '<!-- end ngRepeat: i in m.item -->' + '<ul name="b2" ng-repeat="i in m.item"></ul>'+ + '<!-- end ngRepeat: i in m.item -->' + '</div>' + + '<!-- end ngRepeat: m in model -->' + '</div>'); })); @@ -306,15 +320,18 @@ describe('Binder', function() { '<div ng-repeat="i in [0,1]" ng-class-even="\'e\'" ng-class-odd="\'o\'"></div>' + '</div>')($rootScope); $rootScope.$apply(); + var d1 = jqLite(element[0].childNodes[1]); - var d2 = jqLite(element[0].childNodes[2]); + var d2 = jqLite(element[0].childNodes[3]); expect(d1.hasClass('o')).toBeTruthy(); expect(d2.hasClass('e')).toBeTruthy(); expect(sortedHtml(element)).toBe( '<div>' + '<!-- ngRepeat: i in [0,1] -->' + '<div class="o" ng-class-even="\'e\'" ng-class-odd="\'o\'" ng-repeat="i in [0,1]"></div>' + + '<!-- end ngRepeat: i in [0,1] -->' + '<div class="e" ng-class-even="\'e\'" ng-class-odd="\'o\'" ng-repeat="i in [0,1]"></div>' + + '<!-- end ngRepeat: i in [0,1] -->' + '</div>'); })); @@ -420,7 +437,9 @@ describe('Binder', function() { '<ul>' + '<!-- ngRepeat: (k,v) in {a:0,b:1} -->' + '<li ng-bind=\"k + v\" ng-repeat="(k,v) in {a:0,b:1}">a0</li>' + + '<!-- end ngRepeat: (k,v) in {a:0,b:1} -->' + '<li ng-bind=\"k + v\" ng-repeat="(k,v) in {a:0,b:1}">b1</li>' + + '<!-- end ngRepeat: (k,v) in {a:0,b:1} -->' + '</ul>'); })); diff --git a/test/helpers/testabilityPatch.js b/test/helpers/testabilityPatch.js index 514a5fdb..41b5042a 100644 --- a/test/helpers/testabilityPatch.js +++ b/test/helpers/testabilityPatch.js @@ -226,6 +226,17 @@ function sortedHtml(element, showNgClass) { } +function childrenTagsOf(element) { + var tags = []; + + forEach(jqLite(element).children(), function(child) { + tags.push(child.nodeName.toLowerCase()); + }); + + return tags; +} + + // TODO(vojta): migrate these helpers into jasmine matchers /**a * This method is a cheap way of testing if css for a given node is not set to 'none'. It doesn't diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index f0989f4a..a788e452 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -166,7 +166,7 @@ describe('ngClass', function() { element = $compile('<ul><li ng-repeat="i in [0,1]" class="existing" ng-class-odd="\'odd\'" ng-class-even="\'even\'"></li><ul>')($rootScope); $rootScope.$digest(); var e1 = jqLite(element[0].childNodes[1]); - var e2 = jqLite(element[0].childNodes[2]); + var e2 = jqLite(element[0].childNodes[3]); expect(e1.hasClass('existing')).toBeTruthy(); expect(e1.hasClass('odd')).toBeTruthy(); expect(e2.hasClass('existing')).toBeTruthy(); @@ -181,7 +181,7 @@ describe('ngClass', function() { '<ul>')($rootScope); $rootScope.$apply(); var e1 = jqLite(element[0].childNodes[1]); - var e2 = jqLite(element[0].childNodes[2]); + var e2 = jqLite(element[0].childNodes[3]); expect(e1.hasClass('plainClass')).toBeTruthy(); expect(e1.hasClass('odd')).toBeTruthy(); @@ -199,7 +199,7 @@ describe('ngClass', function() { '<ul>')($rootScope); $rootScope.$apply(); var e1 = jqLite(element[0].childNodes[1]); - var e2 = jqLite(element[0].childNodes[2]); + var e2 = jqLite(element[0].childNodes[3]); expect(e1.hasClass('A')).toBeTruthy(); expect(e1.hasClass('B')).toBeTruthy(); @@ -273,7 +273,7 @@ describe('ngClass', function() { $rootScope.$digest(); var e1 = jqLite(element[0].childNodes[1]); - var e2 = jqLite(element[0].childNodes[2]); + var e2 = jqLite(element[0].childNodes[3]); expect(e1.hasClass('odd')).toBeTruthy(); expect(e1.hasClass('even')).toBeFalsy(); @@ -295,7 +295,7 @@ describe('ngClass', function() { $rootScope.$digest(); var e1 = jqLite(element[0].childNodes[1]); - var e2 = jqLite(element[0].childNodes[2]); + var e2 = jqLite(element[0].childNodes[3]); expect(e1.hasClass('odd')).toBeTruthy(); expect(e1.hasClass('even')).toBeFalsy(); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 4cf79dbf..8dba13bf 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -621,7 +621,9 @@ describe('ngRepeat', function() { '<div>' + '<!-- ngRepeat: i in items -->' + '<div ng-repeat="i in items" rr="">1|</div>' + + '<!-- end ngRepeat: i in items -->' + '<div ng-repeat="i in items" rr="">2|</div>' + + '<!-- end ngRepeat: i in items -->' + '</div>' ); })); @@ -651,7 +653,9 @@ describe('ngRepeat', function() { '<div>' + '<!-- ngRepeat: i in items -->' + '<div ng-repeat="i in items" rr="">1|</div>' + + '<!-- end ngRepeat: i in items -->' + '<div ng-repeat="i in items" rr="">2|</div>' + + '<!-- end ngRepeat: i in items -->' + '</div>' ); })); @@ -748,6 +752,106 @@ describe('ngRepeat', function() { }); + it('should add separator comments after each item', inject(function ($compile, $rootScope) { + var check = function () { + var children = element.find('div'); + expect(children.length).toBe(3); + + // Note: COMMENT_NODE === 8 + expect(children[0].nextSibling.nodeType).toBe(8); + expect(children[0].nextSibling.nodeValue).toBe(' end ngRepeat: val in values '); + expect(children[1].nextSibling.nodeType).toBe(8); + expect(children[1].nextSibling.nodeValue).toBe(' end ngRepeat: val in values '); + expect(children[2].nextSibling.nodeType).toBe(8); + expect(children[2].nextSibling.nodeValue).toBe(' end ngRepeat: val in values '); + } + + $rootScope.values = [1, 2, 3]; + + element = $compile( + '<div>' + + '<div ng-repeat="val in values">val:{{val}};</div>' + + '</div>' + )($rootScope); + + $rootScope.$digest(); + check(); + + $rootScope.values.shift(); + $rootScope.values.push(4); + $rootScope.$digest(); + check(); + })); + + + it('should remove whole block even if the number of elements inside it changes', inject( + function ($compile, $rootScope) { + + $rootScope.values = [1, 2, 3]; + + element = $compile( + '<div>' + + '<div ng-repeat-start="val in values"></div>' + + '<span>{{val}}</span>' + + '<p ng-repeat-end></p>' + + '</div>' + )($rootScope); + + $rootScope.$digest(); + + var ends = element.find('p'); + expect(ends.length).toBe(3); + + // insert an extra element inside the second block + var extra = angular.element('<strong></strong>')[0]; + element[0].insertBefore(extra, ends[1]); + + $rootScope.values.splice(1, 1); + $rootScope.$digest(); + + // expect the strong tag to be removed too + expect(childrenTagsOf(element)).toEqual([ + 'div', 'span', 'p', + 'div', 'span', 'p' + ]); + })); + + + it('should move whole block even if the number of elements inside it changes', inject( + function ($compile, $rootScope) { + + $rootScope.values = [1, 2, 3]; + + element = $compile( + '<div>' + + '<div ng-repeat-start="val in values"></div>' + + '<span>{{val}}</span>' + + '<p ng-repeat-end></p>' + + '</div>' + )($rootScope); + + $rootScope.$digest(); + + var ends = element.find('p'); + expect(ends.length).toBe(3); + + // insert an extra element inside the third block + var extra = angular.element('<strong></strong>')[0]; + element[0].insertBefore(extra, ends[2]); + + // move the third block to the begining + $rootScope.values.unshift($rootScope.values.pop()); + $rootScope.$digest(); + + // expect the strong tag to be moved too + expect(childrenTagsOf(element)).toEqual([ + 'div', 'span', 'strong', 'p', + 'div', 'span', 'p', + 'div', 'span', 'p' + ]); + })); + + describe('stability', function() { var a, b, c, d, lis; @@ -871,26 +975,53 @@ describe('ngRepeat', function() { }); }); - it('should grow multi-node repeater', inject(function($compile, $rootScope) { - $rootScope.show = false; - $rootScope.books = [ - {title:'T1', description: 'D1'}, - {title:'T2', description: 'D2'} - ]; - element = $compile( + + describe('ngRepeatStart', function () { + it('should grow multi-node repeater', inject(function($compile, $rootScope) { + $rootScope.show = false; + $rootScope.books = [ + {title:'T1', description: 'D1'}, + {title:'T2', description: 'D2'} + ]; + element = $compile( + '<div>' + + '<dt ng-repeat-start="book in books">{{book.title}}:</dt>' + + '<dd ng-repeat-end>{{book.description}};</dd>' + + '</div>')($rootScope); + + $rootScope.$digest(); + expect(element.text()).toEqual('T1:D1;T2:D2;'); + $rootScope.books.push({title:'T3', description: 'D3'}); + $rootScope.$digest(); + expect(element.text()).toEqual('T1:D1;T2:D2;T3:D3;'); + })); + + + it('should not clobber ng-if when updating collection', inject(function ($compile, $rootScope) { + $rootScope.values = [1, 2, 3]; + $rootScope.showMe = true; + + element = $compile( '<div>' + - '<dt ng-repeat-start="book in books">{{book.title}}:</dt>' + - '<dd ng-repeat-end>{{book.description}};</dd>' + - '</div>')($rootScope); + '<div ng-repeat-start="val in values">val:{{val}};</div>' + + '<div ng-if="showMe" ng-repeat-end>if:{{val}};</div>' + + '</div>' + )($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('T1:D1;T2:D2;'); - $rootScope.books.push({title:'T3', description: 'D3'}); - $rootScope.$digest(); - expect(element.text()).toEqual('T1:D1;T2:D2;T3:D3;'); - })); + $rootScope.$digest(); + expect(element.find('div').length).toBe(6); + + $rootScope.values.shift(); + $rootScope.values.push(4); + + $rootScope.$digest(); + expect(element.find('div').length).toBe(6); + expect(element.text()).not.toContain('if:1;'); + })); + }); }); + describe('ngRepeat animations', function() { var body, element, $rootElement; |
