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;  | 
