Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing 6 changed files with 357 additions and 295 deletions.
10 changes: 2 additions & 8 deletions js/angular/directive/collectionRepeat.js
Original file line number Diff line number Diff line change
@@ -158,15 +158,9 @@ function($collectionRepeatManager, $collectionDataSource, $parse) {
} else if (!isVertical && !$attr.collectionItemWidth) {
throw new Error(COLLECTION_REPEAT_ATTR_WIDTH_ERROR);
}
$attr.collectionItemHeight = $attr.collectionItemHeight || '"100%"';
$attr.collectionItemWidth = $attr.collectionItemWidth || '"100%"';

var heightParsed = $attr.collectionItemHeight ?
$parse($attr.collectionItemHeight) :
function() { return scrollView.__clientHeight; };
var widthParsed = $attr.collectionItemWidth ?
$parse($attr.collectionItemWidth) :
function() { return scrollView.__clientWidth; };
var heightParsed = $parse($attr.collectionItemHeight || '"100%"');
var widthParsed = $parse($attr.collectionItemWidth || '"100%"');

var heightGetter = function(scope, locals) {
var result = heightParsed(scope, locals);
112 changes: 64 additions & 48 deletions js/angular/service/collectionRepeatDataSource.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ IonicModule
'$parse',
'$rootScope',
function($cacheFactory, $parse, $rootScope) {
var nextCacheId = 0;

function CollectionRepeatDataSource(options) {
var self = this;
this.scope = options.scope;
@@ -35,33 +35,28 @@ function($cacheFactory, $parse, $rootScope) {
};
}

var cacheKeys = {};
this.itemCache = $cacheFactory(nextCacheId++, {size: 500});

var _put = this.itemCache.put;
this.itemCache.put = function(key, value) {
cacheKeys[key] = true;
return _put(key, value);
};

var _remove = this.itemCache.remove;
this.itemCache.remove = function(key) {
delete cacheKeys[key];
return _remove(key);
};
this.itemCache.keys = function() {
return Object.keys(cacheKeys);
};
this.attachedItems = {};
this.BACKUP_ITEMS_LENGTH = 10;
this.backupItemsArray = [];
}
CollectionRepeatDataSource.prototype = {
setup: function() {
for (var i = 0; i < this.BACKUP_ITEMS_LENGTH; i++) {
this.detachItem(this.createItem());
}
},
destroy: function() {
this.dimensions.length = 0;
this.itemCache.keys().forEach(function(key) {
var item = this.itemCache.get(key);
item.element.remove();
item.scope.$destroy();
this.data = null;
forEach(this.backupItemsArray, function(item) {
this.destroyItem(item);
}, this);
this.backupItemsArray.length = 0;

forEach(this.attachedItems, function(item, key) {
this.destroyItem(item);
}, this);
this.itemCache.removeAll();
this.attachedItems = {};
},
calculateDataDimensions: function() {
var locals = {};
@@ -74,53 +69,74 @@ function($cacheFactory, $parse, $rootScope) {
};
}, this);
},
compileItem: function(index, value) {
var key = this.itemHashGetter(index, value);
var cachedItem = this.itemCache.get(key);
if (cachedItem) return cachedItem;

createItem: function() {
var item = {};
item.scope = this.scope.$new();
item.scope[this.keyExpr] = value;

this.transcludeFn(item.scope, function(clone) {
clone.css('position', 'absolute');
item.element = clone;
});

return this.itemCache.put(key, item);
this.transcludeParent.append(item.element);

return item;
},
getItem: function(index) {
getItem: function(hash) {
window.AMOUNT = window.AMOUNT || 0;
if ( (item = this.attachedItems[hash]) ) {
//do nothing, the item is good
} else if ( (item = this.backupItemsArray.pop()) ) {
reconnectScope(item.scope);
} else {
AMOUNT++;
item = this.createItem();
}
return item;
},
attachItemAtIndex: function(index) {
var value = this.data[index];
var item = this.compileItem(index, value);
var hash = this.itemHashGetter(index, value);
var item = this.getItem(hash);

if (item.scope.$index !== index) {
if (item.scope.$index !== index || item.scope[this.keyExpr] !== value) {
item.scope[this.keyExpr] = value;
item.scope.$index = index;
item.scope.$first = (index === 0);
item.scope.$last = (index === (this.getLength() - 1));
item.scope.$middle = !(item.scope.$first || item.scope.$last);
item.scope.$odd = !(item.scope.$even = (index&1) === 0);

//We changed the scope, so digest if needed
if (!$rootScope.$$phase) {
item.scope.$digest();
}
}

item.hash = hash;
this.attachedItems[hash] = item;

return item;
},
detachItem: function(item) {
var i, node, parent;
//Don't .remove(), that will destroy element data
for (i = 0; i < item.element.length; i++) {
node = item.element[i];
parent = node.parentNode;
parent && parent.removeChild(node);
}
//Don't .$destroy(), just stop watchers and events firing
disconnectScope(item.scope);
destroyItem: function(item) {
item.element.remove();
item.scope.$destroy();
item.scope = null;
item.element = null;
},
attachItem: function(item) {
if (!item.element[0].parentNode) {
this.transcludeParent[0].appendChild(item.element[0]);
detachItem: function(item) {
delete this.attachedItems[item.hash];

// If we are at the limit of backup items, just get rid of the this element
if (this.backupItemsArray.length >= this.BACKUP_ITEMS_LENGTH) {
this.destroyItem(item);
// Otherwise, add it to our backup items
} else {
this.backupItemsArray.push(item);
item.element.css(ionic.CSS.TRANSFORM, 'translate3d(-2000px,-2000px,0)');
//Don't .$destroy(), just stop watchers and events firing
disconnectScope(item.scope);
}
reconnectScope(item.scope);
!$rootScope.$$phase && item.scope.$digest();
},
getLength: function() {
return this.data && this.data.length || 0;
150 changes: 112 additions & 38 deletions js/angular/service/collectionRepeatManager.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@ IonicModule
'$rootScope',
'$timeout',
function($rootScope, $timeout) {
/**
* Vocabulary: "primary" and "secondary" size/direction/position mean
* "y" and "x" for vertical scrolling, or "x" and "y" for horizontal scrolling.
*/
function CollectionRepeatManager(options) {
var self = this;
this.dataSource = options.dataSource;
@@ -13,14 +17,14 @@ function($rootScope, $timeout) {
this.isVertical = !!this.scrollView.options.scrollingY;
this.renderedItems = {};

this.lastRenderScrollValue = this.bufferTransformOffset = this.hasBufferStartIndex =
this.hasBufferEndIndex = this.bufferItemsLength = 0;
this.setCurrentIndex(0);

//Override scrollview's render callback
this.scrollView.__$callback = this.scrollView.__callback;
this.scrollView.__callback = angular.bind(this, this.renderScroll);

function getViewportSize() { return self.viewportSize; }
//Set getters and setters to match whether this scrollview is vertical or not
if (this.isVertical) {
this.scrollView.options.getContentHeight = getViewportSize;

@@ -78,43 +82,68 @@ function($rootScope, $timeout) {
this.removeItem(i);
}
},

/*
* Pre-calculate the position of all items in the data list.
* Do this using the provided width and height (primarySize and secondarySize)
* provided by the dataSource.
*/
calculateDimensions: function() {
/*
* For the sake of explanations below, we're going to pretend we are scrolling
* vertically: Items are laid out with primarySize being height,
* secondarySize being width.
*/
var primaryPos = 0;
var secondaryPos = 0;
var len = this.dataSource.dimensions.length;
var secondaryScrollSize = this.secondaryScrollSize();
var previous;
var previousItem;

return this.dataSource.dimensions.map(function(dim) {
//Each dimension is an object {width: Number, height: Number} provided by
//the dataSource
var rect = {
//Get the height out of the dimension object
primarySize: this.primaryDimension(dim),
//Max out the item's width to the width of the scrollview
secondarySize: Math.min(this.secondaryDimension(dim), secondaryScrollSize)
};

if (previous) {
secondaryPos += previous.secondarySize;
if (previous.primaryPos === primaryPos &&
//If this isn't the first item
if (previousItem) {
//Move the item's x position over by the width of the previous item
secondaryPos += previousItem.secondarySize;
//If the y position is the same as the previous item and
//the x position is bigger than the scroller's width
if (previousItem.primaryPos === primaryPos &&
secondaryPos + rect.secondarySize > secondaryScrollSize) {
//Then go to the next row, with x position 0
secondaryPos = 0;
primaryPos += previous.primarySize;
} else {
primaryPos += previousItem.primarySize;
}
}

rect.primaryPos = primaryPos;
rect.secondaryPos = secondaryPos;

previous = rect;
previousItem = rect;
return rect;
}, this);
},
resize: function() {
this.dimensions = this.calculateDimensions();
var last = this.dimensions[this.dimensions.length - 1];
this.viewportSize = last ? last.primaryPos + last.primarySize : 0;
var lastItem = this.dimensions[this.dimensions.length - 1];
this.viewportSize = lastItem ? lastItem.primaryPos + lastItem.primarySize : 0;
this.setCurrentIndex(0);
this.render(true);
if (!this.dataSource.backupItemsArray.length) {
this.dataSource.setup();
}
},
/*
* setCurrentIndex: set the index in the list that matches the scroller's position.
* Also save the position in the scroller for next and previous items (if they exist)
*/
setCurrentIndex: function(index, height) {
this.currentIndex = index;

@@ -127,22 +156,41 @@ function($rootScope, $timeout) {
this.nextPos = this.dimensions[index + 1].primaryPos;
}
},
/**
* override the scroller's render callback to check if we need to
* re-render our collection
*/
renderScroll: ionic.animationFrameThrottle(function(transformLeft, transformTop, zoom, wasResize) {
if (this.isVertical) {
transformTop = this.getTransformPosition(transformTop);
this.renderIfNeeded(transformTop);
} else {
transformLeft = this.getTransformPosition(transformLeft);
this.renderIfNeeded(transformLeft);
}
return this.scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
}),
getTransformPosition: function(transformPos) {
if ((this.hasNextIndex && transformPos >= this.nextPos) ||
(this.hasPrevIndex && transformPos < this.previousPos) ||
Math.abs(transformPos - this.lastRenderScrollValue) > 100) {
renderIfNeeded: function(scrollPos) {
if ((this.hasNextIndex && scrollPos >= this.nextPos) ||
(this.hasPrevIndex && scrollPos < this.previousPos)) {
// Math.abs(transformPos - this.lastRenderScrollValue) > 100) {
this.render();
}
return transformPos - this.lastRenderScrollValue;
},
/*
* getIndexForScrollValue: Given the most recent data index and a new scrollValue,
* find the data index that matches that scrollValue.
*
* Strategy (if we are scrolling down): keep going forward in the dimensions list,
* starting at the given index, until an item with height matching the new scrollValue
* is found.
*
* This is a while loop. In the worst case it will have to go through the whole list
* (eg to scroll from top to bottom). The most common case is to scroll
* down 1-3 items at a time.
*
* While this is not as efficient as it could be, optimizing it gives no noticeable
* benefit. We would have to use a new memory-intensive data structure for dimensions
* to fully optimize it.
*/
getIndexForScrollValue: function(i, scrollValue) {
var rect;
//Scrolling up
@@ -158,62 +206,88 @@ function($rootScope, $timeout) {
}
return i;
},
/*
* render: Figure out the scroll position, the index matching it, and then tell
* the data source to render the correct items into the DOM.
*/
render: function(shouldRedrawAll) {
var i;
if (this.currentIndex >= this.dataSource.getLength() || shouldRedrawAll) {
var isOutOfBounds = ( this.currentIndex >= this.dataSource.getLength() );
// We want to remove all the items and redraw everything if we're out of bounds
// or a flag is passed in.
if (isOutOfBounds || shouldRedrawAll) {
for (i in this.renderedItems) {
this.removeItem(i);
}
if (this.currentIndex >= this.dataSource.getLength()) return null;
// Just don't render anything if we're out of bounds
if (isOutOfBounds) return;
}

var rect;
var scrollValue = this.scrollValue();
var scrollDelta = scrollValue - this.lastRenderScrollValue;
// Scroll size = how many pixels are visible in the scroller at one time
var scrollSize = this.scrollSize();
// We take the current scroll value and add it to the scrollSize to get
// what scrollValue the current visible scroll area ends at.
var scrollSizeEnd = scrollSize + scrollValue;
// Get the new start index for scrolling, based on the current scrollValue and
// the most recent known index
var startIndex = this.getIndexForScrollValue(this.currentIndex, scrollValue);

//Make buffer start on previous row
var bufferStartIndex = Math.max(startIndex - 1, 0);
while (bufferStartIndex > 0 &&
(rect = this.dimensions[bufferStartIndex]) &&
// If we aren't on the first item, add one row of items before so that when the user is
// scrolling up he sees the previous item
var renderStartIndex = Math.max(startIndex - 1, 0);
// Keep adding items to the 'extra row above' until we get to a new row.
// This is for the case where there are multiple items on one row above
// the current item; we want to keep adding items above until
// a new row is reached.
while (renderStartIndex > 0 &&
(rect = this.dimensions[renderStartIndex]) &&
rect.primaryPos === this.dimensions[startIndex - 1].primaryPos) {
bufferStartIndex--;
renderStartIndex--;
}
var startPos = this.dimensions[bufferStartIndex].primaryPos;

i = bufferStartIndex;
// Keep rendering items, adding them until we are past the end of the visible scroll area
i = renderStartIndex;
while ((rect = this.dimensions[i]) && (rect.primaryPos - rect.primarySize < scrollSizeEnd)) {
this.renderItem(i, rect.primaryPos - startPos, rect.secondaryPos);
this.renderItem(i, rect.primaryPos, rect.secondaryPos);
i++;
}
var bufferEndIndex = i - 1;
var renderEndIndex = i - 1;

// Remove any items that were rendered and aren't visible anymore
for (i in this.renderedItems) {
if (i < bufferStartIndex || i > bufferEndIndex) {
if (i < renderStartIndex || i > renderEndIndex) {
this.removeItem(i);
}
}

this.setCurrentIndex(startIndex);
this.lastRenderScrollValue = startPos;
},
renderItem: function(dataIndex, primaryPos, secondaryPos) {
var item = this.dataSource.getItem(dataIndex);
// Attach an item, and set its transform position to the required value
var item = this.dataSource.attachItemAtIndex(dataIndex);
if (item && item.element) {
this.dataSource.attachItem(item);
item.element.css(ionic.CSS.TRANSFORM, this.transformString(
primaryPos, secondaryPos, secondaryPos
));
if (item.primaryPos !== primaryPos || item.secondaryPos !== secondaryPos) {
item.element.css(ionic.CSS.TRANSFORM, this.transformString(
primaryPos, secondaryPos
));
item.primaryPos = primaryPos;
item.secondaryPos = secondaryPos;
}
// Save the item in rendered items
this.renderedItems[dataIndex] = item;
} else {
// If an item at this index doesn't exist anymore, be sure to delete
// it from rendered items
delete this.renderedItems[dataIndex];
}
},
removeItem: function(dataIndex) {
// Detach a given item
var item = this.renderedItems[dataIndex];
if (item) {
item.primaryPos = item.secondaryPos = null;
this.dataSource.detachItem(item);
delete this.renderedItems[dataIndex];
}
86 changes: 50 additions & 36 deletions test/html/list-fit.html
Original file line number Diff line number Diff line change
@@ -18,48 +18,62 @@ <h1 class="title">Hi</h1>
</a>
</ion-header-bar>
<ion-content>
<ion-refresher on-refresh="onRefresh()" refreshing-text="Refreshing!"></ion-refresher>
<div class="list">
<div class="item"
<ion-list>
<ion-item
class="item-avatar-left item-icon-right"
ng-click="alert(item)"
collection-repeat="item in items"
collection-item-height="52"
collection-item-width="120 + 2*($index % 40)"
ng-style="{width: 120 + 2*($index % 40)}">
{{item}}
</div>
</div>
collection-item-height="85"
collection-item-width="'100%'"
style="position: absolute; left: 0; right: 0;">
<img ng-src="{{item.image}}">
<h2>{{item.text}}</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis porttitor diam urna, vitae consectetur lectus aliquet quis.</p>
<i class="icon" style="color:red; font-size: 30px;" ng-class="['ion-ios7-person','ion-person','ion-android-contact','ion-android-social-user','ion-person-stalker'][$index % 5]"></i>
</ion-item>
</ion-list>
</ion-content>
<script>
function MainCtrl($scope, $ionicScrollDelegate, $timeout) {
$scope.items = [];
for (var i = 0; i < 5000; i++) {
$scope.items.push('item '+i);
}

$scope.height = function(n) {
return 105 + (n % 40);
};
$scope.width = function(n) {
return 105 + (n % 40);
};
var dataUris = {};
function convertImgToBase64(url, callback, outputFormat){
var canvas = document.createElement('CANVAS'),
ctx = canvas.getContext('2d'),
img = new Image;
img.crossOrigin = 'Anonymous';
img.onload = function(){
var dataURL;
canvas.height = img.height;
canvas.width = img.width;
ctx.drawImage(img,0,0);
dataURL = canvas.toDataURL(outputFormat || 'image/png');
callback.call(this, dataURL);
canvas = null;
};
img.src = url;
}
function MainCtrl($scope, $ionicScrollDelegate, $timeout, $q, $ionicLoading) {
var images = [];

//$scope.alert = alert.bind(window);
$ionicLoading.show({
template: 'Loading images...'
});
var deferred;
for (var i = 0; i < 5; i++) {
deferred = $q.defer();
convertImgToBase64('http://placekitten.com/'+(40+(10*i))+'/'+(40+(10*i)), deferred.resolve);
images.push(deferred.promise);
}

$scope.onRefresh = function() {
$timeout(function() {
var len = $scope.items.length;
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.items.unshift(2999 - $scope.items.length);
$scope.$broadcast('scroll.refreshComplete');
}, 1500);
};
$q.all(images).then(function(dataUrls) {
$scope.items = [];
for (var item = 0; item < 5000; item++) {
$scope.items.push({
text: 'Item ' + item,
image: dataUrls[item % 5]
});
}
$timeout($ionicLoading.hide, 200);
});

$scope.scrollBottom = $ionicScrollDelegate.scrollBottom;
}
226 changes: 115 additions & 111 deletions test/unit/angular/service/collectionDataSource.unit.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ describe('$collectionDataSource service', function() {
return dataSource;
}

it('should have properties', function() {
it('should have properties', inject(function($collectionDataSource) {
var source = setup({
scope: 1,
transcludeFn: 2,
@@ -44,7 +44,7 @@ describe('$collectionDataSource service', function() {
expect(source.trackByExpr).toBe(6);
expect(source.heightGetter).toBe(7);
expect(source.widthGetter).toBe(8);
});
}));

describe('.itemHashGetter()', function() {

@@ -70,40 +70,18 @@ describe('$collectionDataSource service', function() {
});
});

describe('itemCache', function() {

it('should be $cacheFactory', function() {
var cache = {};
module('ionic', function($provide) {
$provide.value('$cacheFactory', function() { return cache; });
});
var source = setup();
expect(source.itemCache).toBe(cache);
});

it('should have added keys() method', function() {
var source = setup();
source.itemCache.put('a', 1);
source.itemCache.put('b', 2);
expect(source.itemCache.keys()).toEqual(['a', 'b']);
source.itemCache.remove('a');
expect(source.itemCache.keys()).toEqual(['b']);
});

});

it('.destroy() should cleanup dimensions & cache', function() {
it('.destroy() should cleanup dimensions backupItemsArray and attachedItems', function() {
var source = setup();
source.dimensions = [1,2,3];
var cachedItem = {
scope: { $destroy: jasmine.createSpy('$destroy') },
element: { remove: jasmine.createSpy('remove') }
};
source.itemCache.put('a', cachedItem);
source.attachedItems = {0: 'a'};
source.backupItemsArray = ['b'];
spyOn(source, 'destroyItem');
source.destroy();
expect(source.dimensions.length).toBe(0);
expect(cachedItem.scope.$destroy).toHaveBeenCalled();
expect(cachedItem.element.remove).toHaveBeenCalled();
expect(source.destroyItem).toHaveBeenCalledWith('a');
expect(source.destroyItem).toHaveBeenCalledWith('b');
expect(source.attachedItems).toEqual({});
expect(source.backupItemsArray).toEqual([]);
});

it('.calculateDataDimensions()', function() {
@@ -135,118 +113,144 @@ describe('$collectionDataSource service', function() {
});
});

describe('.compileItem()', function() {
it('should get item from cache if exists', function() {
describe('.createItem()', function() {
it('should return item with new scope and transclude', function() {
var source = setup();
var hash = source.itemHashGetter(1,2);
var item = {};
source.itemCache.put(hash, item);
expect(source.compileItem(1,2)).toBe(item);
});

it('should give back a compiled item and put in cache', function() {
var source = setup({
keyExpr: 'key'
});

var item = source.compileItem(1, 2);
var item = source.createItem();

expect(item.scope.$parent).toBe(source.scope);
expect(item.scope.key).toBe(2);

expect(item.element).toBeTruthy();
expect(item.element.css('position')).toBe('absolute');
expect(item.element.scope()).toBe(item.scope);

expect(source.itemCache.get(source.itemHashGetter(1,2))).toBe(item);
expect(item.element.parent()[0]).toBe(source.transcludeParent[0]);
});
});

describe('.getItem()', function() {
it('should return a value with index values set', function() {
it('should return attachedItems[hash] if available', function() {
var source = setup();
source.data = ['a', 'b', 'c'];
spyOn(source, 'compileItem').andCallFake(function() { return { scope: {} }; });

var item = source.getItem(0);
expect(item.scope.$index).toBe(0);
expect(item.scope.$first).toBe(true);
expect(item.scope.$last).toBe(false);
expect(item.scope.$middle).toBe(false);
expect(item.scope.$odd).toBe(false);

item = source.getItem(1);
expect(item.scope.$index).toBe(1);
expect(item.scope.$first).toBe(false);
expect(item.scope.$last).toBe(false);
expect(item.scope.$middle).toBe(true);
expect(item.scope.$odd).toBe(true);

item = source.getItem(2);
expect(item.scope.$index).toBe(2);
expect(item.scope.$first).toBe(false);
expect(item.scope.$last).toBe(true);
expect(item.scope.$middle).toBe(false);
expect(item.scope.$odd).toBe(false);
var item = {};
source.attachedItems['123'] = item;
expect(source.getItem('123')).toBe(item);
});
});

describe('.detachItem()', function() {
it('should remove element from parent and disconnectScope', function() {
it('should return backupItemsArray item if available, and reconnect the item', function() {
var source = setup();
var element = angular.element('<div>');
var parent = angular.element('<div>').append(element);
var item = {
element: element,
scope: {}
scope: {},
};
spyOn(window, 'disconnectScope');
spyOn(window, 'reconnectScope');
source.backupItemsArray = [item];
expect(source.getItem('123')).toBe(item);
expect(reconnectScope).toHaveBeenCalledWith(item.scope);
});

expect(element[0].parentNode).toBe(parent[0]);
source.detachItem(item);
expect(element[0].parentNode).toBeFalsy();
expect(disconnectScope).toHaveBeenCalledWith(item.scope);
it('should last resort create an item', function() {
var source = setup();
var item = {};
spyOn(source, 'createItem').andReturn(item);
expect(source.getItem('123')).toBe(item);
});
});

describe('.attachItem()', function() {
it('should add element if it has no parent and digest', inject(function($rootScope) {
describe('.attachItemAtIndex()', function() {
it('should return a value with index values set and put in attachedItems', inject(function($rootScope) {
var source = setup({
transcludeParent: angular.element('<div>')
keyExpr: 'value'
});
var element = angular.element('<div>');
spyOn(window, 'reconnectScope');
source.data = ['a', 'b', 'c'];
spyOn(source, 'getItem').andCallFake(function() {
return { scope: $rootScope.$new() };
});
spyOn(source, 'itemHashGetter').andCallFake(function(index, value) {
return index + ':' + value;
});

var item1 = source.attachItemAtIndex(0);
expect(item1.scope.value).toEqual('a');
expect(item1.scope.$index).toBe(0);
expect(item1.scope.$first).toBe(true);
expect(item1.scope.$last).toBe(false);
expect(item1.scope.$middle).toBe(false);
expect(item1.scope.$odd).toBe(false);
expect(item1.hash).toEqual('0:a');

var item2 = source.attachItemAtIndex(1);
expect(item2.scope.value).toEqual('b');
expect(item2.scope.$index).toBe(1);
expect(item2.scope.$first).toBe(false);
expect(item2.scope.$last).toBe(false);
expect(item2.scope.$middle).toBe(true);
expect(item2.scope.$odd).toBe(true);
expect(item2.hash).toEqual('1:b');

var item3 = source.attachItemAtIndex(2);
expect(item3.scope.value).toEqual('c');
expect(item3.scope.$index).toBe(2);
expect(item3.scope.$first).toBe(false);
expect(item3.scope.$last).toBe(true);
expect(item3.scope.$middle).toBe(false);
expect(item3.scope.$odd).toBe(false);
expect(item3.hash).toEqual('2:c');

expect(source.attachedItems).toEqual({
'0:a': item1,
'1:b': item2,
'2:c': item3
});
}));
});

describe('.detachItem()', function() {
it('should detach item and add to backup array if there is room', function() {
var source = setup();
var item = {
element: element,
scope: $rootScope.$new()
element: angular.element('<div>'),
scope: {},
hash: 'foo'
};
source.backupItemsArray = [];
source.attachedItems[item.hash] = item;
spyOn(window, 'disconnectScope');
source.detachItem(item);
expect(source.attachedItems).toEqual({});
expect(source.backupItemsArray).toEqual([item]);
expect(disconnectScope).toHaveBeenCalledWith(item.scope);
});
it('should remove element from parent and disconnectScope if backupItemsArray is full', function() {
var source = setup();
spyOn(source, 'destroyItem');
source.BACKUP_ITEMS_LENGTH = 0;

spyOn(item.scope, '$digest');
spyOn(source.transcludeParent[0], 'appendChild');
source.attachItem(item);
expect(source.transcludeParent[0].appendChild).toHaveBeenCalledWith(element[0]);
expect(reconnectScope).toHaveBeenCalledWith(item.scope);
expect(item.scope.$digest).toHaveBeenCalled();
}));
var item = { hash: 'abc' };
source.attachedItems[item.hash] = item;
source.detachItem(item);
expect(source.destroyItem).toHaveBeenCalledWith(item);
expect(source.attachedItems).toEqual({});
});
});

it('should not append element if it has a parent already', inject(function($rootScope) {
describe('.destroyItem()', function() {
it('should remove element and destroy scope', function() {
var source = setup();
var element = angular.element('<div>');
var source = setup({
transcludeParent: angular.element('<div>')
.append(element)
});
spyOn(window, 'reconnectScope');
var parent = angular.element('<div>').append(element);
var item = {
element: element,
scope: $rootScope.$new()
scope: {}
};
spyOn(item.scope, '$digest');
source.attachItem(item);
spyOn(source.transcludeParent[0], 'appendChild');
expect(source.transcludeParent[0].appendChild).not.toHaveBeenCalled();
expect(reconnectScope).toHaveBeenCalledWith(item.scope);
expect(item.scope.$digest).toHaveBeenCalled();
}));
var destroySpy = item.scope.$destroy = jasmine.createSpy('$destroy');

expect(element[0].parentNode).toBe(parent[0]);
source.destroyItem(item);
expect(element[0].parentNode).toBeFalsy();
expect(destroySpy).toHaveBeenCalled();
expect(item.scope).toBe(null);
expect(item.element).toBe(null);
});
});

describe('.getLength()', function() {
68 changes: 14 additions & 54 deletions test/unit/angular/service/collectionRepeatManager.unit.js
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ describe('collectionRepeatManager service', function() {
var dataSource = new $collectionDataSource(angular.extend({
scope: $rootScope.$new(),
transcludeParent: angular.element('<div>'),
trancsludeFn: function(scope, cb) {
transcludeFn: function(scope, cb) {
cb($compile('<div></div>')(scope));
},
keyExpr: 'key',
@@ -328,41 +328,23 @@ describe('collectionRepeatManager service', function() {
});

describe('.renderScroll()', function() {
it('with isVertical', function() {
it('should pass the values to __$callback', function() {
var manager = setup();
spyOn(manager, 'getTransformPosition').andReturn('banana');
spyOn(manager.scrollView, '__$callback');
manager.renderScroll(1, 2, 3, 4);
expect(manager.getTransformPosition).toHaveBeenCalledWith(2);
expect(manager.scrollView.__$callback).toHaveBeenCalledWith(1,'banana',3,4);
});

it('with !isVertical', function() {
var manager = setup();
manager.isVertical = false;
spyOn(manager, 'getTransformPosition').andReturn('blueberry');
spyOn(manager.scrollView, '__$callback');
manager.renderScroll(1, 2, 3, 4);
expect(manager.getTransformPosition).toHaveBeenCalledWith(1);
expect(manager.scrollView.__$callback).toHaveBeenCalledWith('blueberry',2,3,4);
expect(manager.scrollView.__$callback).toHaveBeenCalledWith(1, 2, 3, 4);
});
});

describe('.getTransformPosition()', function() {
it('should return pos - lastRenderScrollValue', function() {
var manager = setup();
manager.lastRenderScrollValue = 11;
expect(manager.getTransformPosition(44)).toBe(33);
});

describe('.renderIfNeeded()', function() {
it('should render if >= nextPos', function() {
var manager = setup();
spyOn(manager, 'render');
manager.hasNextIndex = true;
manager.nextPos = 30;
manager.getTransformPosition(20);
manager.renderIfNeeded(20);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(30);
manager.renderIfNeeded(30);
expect(manager.render).toHaveBeenCalled();
});

@@ -371,31 +353,11 @@ describe('collectionRepeatManager service', function() {
spyOn(manager, 'render');
manager.hasPrevIndex = true;
manager.previousPos = 50;
manager.getTransformPosition(60);
manager.renderIfNeeded(60);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(50);
manager.renderIfNeeded(50);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(49);
expect(manager.render).toHaveBeenCalled();
});

it('should render if abs(val)>100', function() {
var manager = setup();
spyOn(manager, 'render');
manager.getTransformPosition(60);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(100);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(101);
expect(manager.render).toHaveBeenCalled();

manager.render.reset();

manager.getTransformPosition(-60);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(-100);
expect(manager.render).not.toHaveBeenCalled();
manager.getTransformPosition(-101);
manager.renderIfNeeded(49);
expect(manager.render).toHaveBeenCalled();
});
});
@@ -425,7 +387,7 @@ describe('collectionRepeatManager service', function() {
var manager = setup();
manager.renderedItems = {'a':1, 'b':1};
spyOn(manager, 'removeItem');
expect(manager.render()).toBe(null);
manager.render();
expect(manager.removeItem).toHaveBeenCalledWith('a');
expect(manager.removeItem).toHaveBeenCalledWith('b');
});
@@ -522,16 +484,15 @@ describe('collectionRepeatManager service', function() {
});

describe('.renderItem()', function() {
it('should attachItem and set the element transform', function() {
it('should attachItemAtIndex and set the element transform', function() {
var manager = setup();
var item = {
element: angular.element('<div>')
};
spyOn(item.element, 'css');
spyOn(manager.dataSource, 'getItem').andReturn(item);
spyOn(manager.dataSource, 'attachItem');
spyOn(manager.dataSource, 'attachItemAtIndex').andReturn(item);
manager.renderItem(0, 33, 44);
expect(manager.dataSource.attachItem).toHaveBeenCalledWith(item);
expect(manager.dataSource.attachItemAtIndex).toHaveBeenCalledWith(0);
expect(item.element.css).toHaveBeenCalledWith(
ionic.CSS.TRANSFORM,
manager.transformString(33, 44)
@@ -545,10 +506,9 @@ describe('collectionRepeatManager service', function() {
var manager = setup();
var item = {};
manager.renderedItems[0] = item;
spyOn(manager.dataSource, 'getItem').andReturn(item);
spyOn(manager, 'removeItem').andCallThrough();
spyOn(manager.dataSource, 'detachItem');
manager.removeItem(0);
expect(manager.dataSource.detachItem).toHaveBeenCalledWith(item);
expect(manager.renderedItems).toEqual({});
});
});

0 comments on commit 6af5d68

Please sign in to comment.