Infinite Ajax Carousel
August 10, 2010 | James Brantly
In a forthcoming demo app of a shopping browser, we'll be showcasing an "infinite carousel", a new feature of our carousel component that we'll be releasing as part of Sencha Touch. The major feature of the infinite carousel is active DOM management that keeps the web app within the iPad's memory limits.
Here I'd like to walk you through some of the more interesting technical aspects that went into its creation. The biggest challenges revolved around the fact that in our shopping browser, a single carousel could contain literally thousands of images. Simply loading these all into memory at once was a guaranteed browser crash. So we needed to make sure that certain portions of the application were able to scale correctly.
Creating An Infinite Carousel
The infinite carousel can handle thousands of items. It does this by using a sliding window for objects it keeps in memory. For the currently displayed card, the immediate preceding and following cards are also created. As the user scrolls through, items are created and destroyed at each end as needed so that the window is always kept up-to-date. With this method, we can ensure that the number of created items is below some fixed constant regardless of how many items are actually in the carousel. In the diagram below, the sliding window size is 3 items. Initially it is centered on item 3 and only items 2 through 4 have actually been created. When the user changes the current item to 4, the window slides right. Item 2 is removed and item 5 is created and added to the carousel.

The API for using this carousel is pretty simple. Instead of using an items config property, there is a createItem config property which accepts a function. Whenever an item needs to be created, the function will be called with the appropriate index. The function then creates and returns the item. To demonstrate this I'll show two ways to accomplish the same thing: one with a normal carousel and one with our infinite carousel.
// Normal carousel
var carousel = new Ext.Carousel({
items: [{
html: '1'
},{
html: '2'
}, {
html: '3'
}]
});
// Infinite carousel
var carousel = new SS.BufferedCarousel({
itemCount: 3,
createItem: function(index) {
return {html: (i+1)+''};
}
});
Conceptually, the sliding window is pretty simple. On initial load and whenever the visible item is changed, update the window. So the meat of the implementation is in the window update code. It is called with the index of the item that the window should be centered on. Here is the source code in full:
bufferCards: function(index) {
// Quick return if there is nothing to do
if (this.lastBufferedIndex == index) { return; }
this.lastBufferedIndex = index;
// Initialize variables
var
// size of the window
bufferSize = this.bufferSize,
// constrained start index of the window
start = (index-bufferSize).constrain(0, this.itemCount-1),
// constrained end index of the window
end = (index+bufferSize).constrain(0, this.itemCount-1),
items = this.items,
// flag to determine if any items were added/removed
changed = false,
// will be set to the item where its position == index
activeCard;
// make sure the index is within bounds
index = index.constrain(0, this.itemCount-1);
// cull existing items
var i = 0;
while (i < items.length) {
var item = items.get(i),
itemIndex = item.carouselPosition;
if (itemIndex end) {
this.remove(item, true);
changed = true;
}
else {
i++;
}
}
// function to create a card and add to the carousel
var createCard = function(carouselPos, layoutPos) {
var card = this.createItem(i);
if (card) {
card.carouselPosition = carouselPos;
if (layoutPos != null) {
this.insert(layoutPos, card);
}
else {
this.add(card);
}
if (carouselPos == index) {
activeCard = card;
}
changed = true;
}
};
// add new items
if (items.length) { // if existing items, add to the left and right
var first = items.first().carouselPosition,
last = items.last().carouselPosition;
for (var i = first-1; i>=start; i--) {
if (i >= 0) {
createCard.call(this, i, 0);
}
}
for (var i = last+1; i<=end; i++) {
createCard.call(this, i);
}
}
else { // if no existing items, just add cards
for (var i = start; i= 0) {
createCard.call(this, i);
}
}
}
// if changed, make sure the layout is updated
// also, update the active item if needed
if (changed) {
this.doLayout();
var activeItem = this.layout.getActiveItem();
if (activeCard && activeItem != activeCard) {
this.layout.setActiveItem(activeCard);
}
}
}
Custom Carousel Indicator
In an infinite carousel the position indicator for a normal carousel does not work. To remedy this we created a position indicator component which looks more like a normal Touch scrollbar. Using the standard indicator as a template, the code is fairly straightforward:
SS.PagedCarousel.Indicator = Ext.extend(Ext.Component, {
baseCls: "ss-pagedCarousel-indicator",
initComponent: function() {
if (this.carousel.rendered) {
this.render(this.carousel.body);
}
else {
this.carousel.on('render', function() {
this.render(this.carousel.body);
}, this, {single: true});
}
},
onRender: function() {
SS.PagedCarousel.Indicator.superclass.onRender.apply(this, arguments);
this.positionIndicator = this.el.createChild({tag: 'span'});
},
onBeforeCardSwitch: function(carousel, card) {
if (card) {
var position = card.carouselPosition/(this.carousel.itemCount-1),
position = isNaN(position) ? 0 : position*100,
el = this.el;
this.positionIndicator[this.carousel.direction=='vertical'?'setTop':'setLeft'](position.toFixed(2)+"%");
el.setStyle('opacity', '1');
if (this.hideTimeout != null) {
clearTimeout(this.hideTimeout);
}
this.hideTimeout = setTimeout(function() {
el.setStyle('opacity', '0');
}, 1500);
}
},
});
Data Retrieval and Precaching
The backend for this demo provides an API that can be used to retrieve and filter products from categories. It also provides a way to page the data so that it can be retrieved in chunks as needed. In the app, however, we wanted to make sure that we always had the data we need as soon as we needed it. When rendering an item to the carousel, we didn't want to wait for network latency. This meant that we needed to precache images.
So, we created a separate class called a DataCache to ease the process of retrieving and caching products. At its heart, DataCache contains a simple function called “getItems” which accepts a range of products and a callback function for handling the products once they are retrieved. In addition, “getItems” has some simple logic whereby calls for data that have already been requested do not trigger an additional request over the wire. And calls for data that has already been received returns the data immediately. The result is that handling precaching becomes extremely easy. All we need to do is call getItems for a chunk of products before the app needs to display those products and the rest is taken care of automatically. This process is best shown in a diagram:

When getItems is called for the first 100 items, a request is made to the backend for those items. When the subsequent calls to getItems for items 1-10 and 11-20 are made, the app detects that these have already been asked for and no request is made to the backend API. Once the response from the backend comes back, all outstanding callbacks depending on that data are invoked. If later on getItems is used for items 21-30, the callback is instantly invoked since the data has already been retrieved. Using the data cache greatly simplifies the efficient retrieval of data throughout the rest of the application.
Fun with XTemplate
When displaying products, each carousel page shows 8 or 9 (depending on orientation) products in a grid layout. Most of the time people use floating elements to provide a grid layout which will automatically stack side by side and wrap when necessary. However, in this case we really wanted to use CSS3 flexible box layout since we can take advantage of a modern webkit browser. To do this, we need to create wrapper elements within the HTML that act as containers around every 2 or 3 products, and we want to do this within a single loop. Here is the resulting code:
landscapeProductTpl: new Ext.XTemplate(
'<div class="productlist-container">',
'',
'{[ xindex == 1 || xindex % 2 == 1 ? "<div>" : ""]}',
'<div>',
'<div class="productlist-item-img boxFlexible">{images:this.renderImage}</div>',
'<div class="productlist-item-text">',
'<span>{name}</span>',
'<span>{price:this.renderPrice} - {retailer}</span>',
'</div>',
'</div>',
'{[ xindex == xcount && xindex % 2 == 1 ? "<div></div>" : ""]}',
'{[ xindex == xcount || xindex % 2 == 0 ? "</div>" : ""]}',
'',
'</div>',
templateFunctions
)
Note the use of inline Javascript to conditionally insert divs before and after each product. Through careful use of the xindex and xcount variables and some modulus magic, this template will create a wrapper div around every 2 products. This wrapper div uses the CSS3 vertical box functionality to stack the two products on top of each other. The end result is a 2x4 grid when 8 products are fed into the template.
The Power of Mobile Web
The infinite carousel demonstrates the power of Sencha Touch to create application experiences that meet the bar that's been set by native applications. And best of all, because it's web based, this should also run on other tablets like the expected HP Tablet based on WebOS and Android Tablets (once we add support).
We hope to launch the shopping browser demo app soon. Then you'll be able to see the demo in action!
There are 23 responses. Add yours.
jeroenvduffelen
2 years agoCool! We are implementing this exact same feature - but with the DataView, for more flexibility of the scroll area!
Awesomee Bob
2 years agoCool beans. Glad to see that performance and functionality can still be achieved at the same time. Keep them coming!
Rafael Ferreira
2 years agoWe implemented exactly the same feature a 2 years ago.
Douglas Bonneville
2 years agoWill it have inertia scrolling? For example, if you flick to the left or right, will the UI scroll with inertia and then let the images catch up with where the carousel is? Would it work essentially like the iTunes or OSX preview “cover flow”?
Tof
2 years agoGNi ?!
xindex == 1 || xindex % 2 == 1
JeeShen Lee
2 years agoCan’t wait to check out the DEMO!
Animal
2 years agoUse XTemplate member functions to calculate the <divs> and keep the template a bit simpler.
Nasty code in Javascript not inside a template:
</code>
var t = new Ext.XTemplate(’<tpl for=”.”>’ +
’{[this.getTopTag(xindex)]}’ +
’{.}’ +
’{[this.getBottomTag(xindex, xcount)]}’ +
’</tpl>’,
{
getTopTag: function(idx){
return idx == 1 || idx % 2 == 1 ? “<div>” : “”;
},
getBottomTag: function(idx, xcount){
var result = idx == xcount && idx % 2 == 1 ? “<div></div>” : “”;
if (idx == xcount || idx % 2 == 0) {
result += ‘<div>’;
}
return result;
}
});
t.apply([1, 2, 3, 4, 5, 6]);
</code>
Jackson
2 years agoAwesome! Thanks for the article..
Juergen
2 years agoWhen do you expect to lunch this feature ?
SenchaUser
2 years agoAny updates on the demo?
Michael Mullany
2 years agoYou can see the demo staged here:
www.touchstyle.mobi/app/
Enjoy!!
SenchaUser
2 years agoI’m using the code from your example, just updated to new version (0.97) and it seems like this.doLayout(); is breaking. Any ideas how to fix it?
Marcel
2 years agocan sencha touch carousel use multitouch
see here: touch.sproutcore.com
Blippo
2 years agoPlease could post an example for the iPhone I could not’m starting
ml
1 year agoif (itemIndex end) {
this.remove(item, true);
changed = true;
}
else {
i++;
}
}
should it say (if (itemIndex == end) ?
norabora
1 year agoI found that your demo site (http://www.touchstyle.mobi/app/) uses old version of Sencha Touch. So when I use BufferedCarousel with 1.0, indicator is now showing. Are you planning to port this demo to Sencha Touch 1.0?
norabora
1 year agoI forgot to copy css about indicator. Sorry about that. One question though, since it is ‘infinite’ carousel, how can I set itemCount? It will be increasing forever.
JackSparrow
1 year ago//Try to this
SS.BufferedCarousel.Indicator = Ext.extend(Ext.Component, {
baseCls: “ss-pagedCarousel-indicator”,
initComponent: function() {
if (this.carousel.rendered) {
this.render(this.carousel.body);
}
else {
this.carousel.on(‘render’, function() {
this.render(this.carousel.body);
}, this, {single: true});
}
},
onRender: function() {
SS.BufferedCarousel.Indicator.superclass.onRender.apply(this, arguments);
this.positionIndicator = this.el.createChild({tag: ‘span’});
},
onBeforeCardSwitch: function(carousel, card) {
if (card) {
var position = card.carouselPosition/(this.carousel.itemCount-1),
position = isNaN(position) ? 0 : position*100,
el = this.el;
this.positionIndicator[this.carousel.direction==‘vertical’?‘setTop’:‘setLeft’](position.toFixed(2)+”%”);
el.setStyle(‘opacity’, ‘1’);
if (this.hideTimeout != null) {
clearTimeout(this.hideTimeout);
}
this.hideTimeout = setTimeout(function() {
el.setStyle(‘opacity’, ‘0’);
}, 1500);
}
},
// @private
createIndicator: function() {
this.indicators = this.indicators || [];
this.indicators.push(this.el.createChild({
tag: ‘span’
}));
},
// @private
onCardAdd: function() {
this.createIndicator();
},
// @private
onCardRemove: function() {
this.indicators.pop().remove();
}
});
JackSparrow
1 year ago//mod SS.BufferedCarousel.Indicator class
//add new 3 functions (from sencha 1.0.1) to this class
// @private
createIndicator: function() {
this.indicators = this.indicators || [];
this.indicators.push(this.el.createChild({
tag: ‘span’
}));
},
// @private
onCardAdd: function() {
this.createIndicator();
},
// @private
onCardRemove: function() {
this.indicators.pop().remove();
}
//enjoin
Ivan Marquez
1 year agoNeed help with this problem, i’m using sencha touch 1.0.1 and use this:
this.carousel = new SS.BufferedCarousel({
itemCount: 3,
initialCarouselPosition: 0,
createItem: function(index) {
return {html: (i+1)+’‘};
},
direction: Ext.getOrientation() == “portrait” ? ‘vertical’ : ‘horizontal’
});
BUt i have this error: Uncaught TypeError: Object -2 has no method ‘constrain’
Justin Dijkshoorn
1 year agoI’ve got the same error as well..
Uncaught TypeError: Object -2 has no method ‘constrain’
its on this line: start = (index-bufferSize).constrain(0, this.itemCount-1),
Is there any way to fix this in 1.0.1?
Erick
9 months ago@Justin To fix that error add this line some where before you instantiate the class:
Number.prototype.constrain = Ext.util.Numbers.constrain;
I am getting another error which is: TypeError: Object has no method ‘onCardAdd’
On sencha debug it’s line 37195
Erick
9 months ago@Justin @Ivan Marquez, I was able to get the BufferedCarousel to work. The previous code I posted, gets rid of the error, but it is incorrect. The correct code should be:
Number.prototype.constrain = function(min, max){return Ext.util.Numbers.constrain(this.valueOf(), min, max);}
I also had to use @JackSparrow’s fix for the indicator class that’s included in the BufferedCarousel.js file. It’s working smoothly now with Sencha 1.1
Comments are Gravatar enabled. Your email address will not be shown.
Commenting is not available in this channel entry.