Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created March 25, 2014 11:13
Creating A Bidirectional Infinite Scroll Page With jQuery And ColdFusion
<!DOCTYPE HTML>
<html>
<head>
<title>Bidirectional Infinite Scroll With jQuery And AJAX</title>
<style type="text/css">
div.list-chunk {
padding-bottom: 1px ;
}
div.list-item {
border: 4px solid #E0E0E0 ;
margin: 0px 0px 13px 0px ;
padding: 10px 10px 10px 10px ;
}
div.list-item a.thumbnail {
float: left ;
height: 150px ;
margin: 0px 15px 10px 0px ;
width: 100px ;
}
div.list-item a.thumbnail img {
border: 1px solid #999999 ;
display: block ;
}
div.list-item h4 {
font-size: 18px ;
margin: 0px 0px 12px 0px ;
}
div.list-item div.offset {
border-top: 1px solid #CCCCCC ;
clear: both ;
font-size: 11px ;
padding-top: 4px ;
}
</style>
<script type="text/javascript" src="../jquery-1.4a2.js"></script>
<script type="text/javascript">
// I get more list items and either prepend them or append
// them to the list depending on the target area.
function getMoreListItems(
container,
targetArea,
onComplete
){
// Check to see if there is any existing AJAX call
// for the list data items. If there is, we want to
// return out of this method - no reason to overload
// the server with extraneous requests (more so than
// an infinite scroll effect already does!!).
if (container.data( "xhr" )){
// Let the active AJAX request complete.
return;
}
// Get the min and max offsets of the current
// container.
var minOffset = (container.data( "minOffset" ) || 0);
var maxOffset = (container.data( "maxOffset" ) || 0);
// The count of list items to load per AJAX request.
// We are calling it a "chunk" size because each
// list chunk will be stored in its own sub-container
// to make DOM manipulation easier.
var chunkSize = 3;
// Check our target area to see what our next offset
// for loading should be.
if (targetArea == "top"){
// We are prepending list items.
var nextOffset = (minOffset - 1 - chunkSize);
} else {
// We are appending list items.
var nextOffset = (maxOffset + 1);
}
// Launch AJAX request for next set of results and
// store the resultant XHR request with the container.
container.data(
"xhr",
$.ajax({
type: "get",
url: "./bidirectional.cfm",
data: {
offset: nextOffset,
count: chunkSize
},
dataType: "json",
success: function( response ){
// Apply the response to the container
// for the given target area.
applyListItems( container, targetArea, response );
},
complete: function(){
// Remove the stored AJAX request. This
// will allow subsequent AJAX requests
// to execute.
container.removeData( "xhr" );
// Call the onComplete callback.
onComplete();
}
})
);
}
// I apply the given AJAX response to the container.
function applyListItems( container, targetArea, items ){
// Get a reference to our HTML template for a new
// list item.
var template = $( "#list-item-template" );
// Create an array to hold our HTML buffer - this will
// be faster than creating individual DOM elements and
// appending them piece-wise.
var htmlBuffer = [];
// Loop over the array to create each list element.
$.each(
items,
function( index, item ){
// Modify the template and append the result
// to the HTML buffer.
htmlBuffer.push(
template.html().replace(
new RegExp( "\\$\\{(src|offset)\\}", "g" ),
function( $0, $1 ){
// Return property.
return( item[ $1.toUpperCase() ] );
}
)
);
}
);
// Create a list chunk which will hold our data.
var chunk = $( "<div class='list-chunk'></div>" );
// Append the list item html buffer to the chunk.
chunk.append( htmlBuffer.join( "" ) );
// Create the min and max offset of the chunk.
chunk.data( "minOffset", items[ 0 ].OFFSET );
chunk.data( "maxOffset", items[ items.length - 1 ].OFFSET );
// Check to see which target area we are adding the
// list items to (top vs. bottom).
if (targetArea == "top"){
// Get the current window scroll before we update
// the list contente.
var viewTop = $( window ).scrollTop();
// Prepend list items.
container.prepend( chunk );
// Now that the chunk has been added to the page,
// it should have a height that can be calculated.
var chunkHeight = chunk.height();
// Re-adjust the scroll of the window to make sure
// the user doesn't suddenly jump to a crazy place.
$( window ).scrollTop( viewTop + chunkHeight );
// Now that we moved the list up, let's remove
// the last chunk from the list.
container.find( "> div.list-chunk:last" ).remove();
} else {
// Append list items.
container.append( chunk );
// Check to see if we have more chunks than we
// want (an arbitrary number, but enough to make
// sure we can comfortable fill the page).
if (container.children().size() > 3){
// We want to remove the first chunk in the
// list to free up some browser memory.
// Get the current window scroll before we
// remove a chunk.
var viewTop = $( window ).scrollTop();
// Get the chunk that we are going to remove.
var oldChunk = container.children( ":first" );
// Get the height of the chunk we are about
// to remove.
var oldChunkHeight = oldChunk.height();
// Remove the hunk.
oldChunk.remove();
// Now, we need to ajust the scroll offset
// of the window so the user is not jumped
// around to a crazy place.
$( window ).scrollTop( viewTop - oldChunkHeight );
}
}
// Now that we have updated the chunks in the
// container, let's update the min / max offsets of
// the container (which will be used on subsequent
// AJAX requests).
container.data(
"minOffset",
container.children( ":first" ).data( "minOffset" )
);
container.data(
"maxOffset",
container.children( ":last" ).data( "maxOffset" )
);
}
// I check to see if more list items are needed based on
// the scroll offset of the window and the position of
// the container. I return a complex result that not only
// determines IF more list items are needed, but on what
// end of the list.
//
// NOTE: These calculate are based ONLY on the offset of
// the list container in the context of the view frame.
// This does not take anything else into account (more
// business logic might be required to see if loading
// truly needs to take place).
function isMoreListItemsNeeded( container ){
// Create a default return. This return value contains
// requirements for both the top and bottom of the
// content list.
var result = {
top: false,
bottom: false
};
// Get the view frame for the window - this is the
// top and bottom coordinates of the visible slice of
// the document.
var viewTop = $( window ).scrollTop();
var viewBottom = (viewTop + $( window ).height());
// Get the offset of the top of the list container.
var containerTop = container.offset().top;
// Get the offset of the bottom of the list container.
var containerBottom = Math.floor(
containerTop + container.height()
);
// I am the scroll buffers for the top and the bottom;
// this is the amount of pre-top and pre-bottom space
// we want to take into account before we start
// loading the next items.
//
// NOTE: The top buffer is a bit bigger only to make
// the transition feel a bit *safer*.
var topScrollBuffer = 500;
var bottomScrollBuffer = 200;
// Check to see if the container top is close enough
// (with buffer) to the top scroll of the view frame
// to trigger loading more items (at the top).
if ((containerTop + topScrollBuffer) >= viewTop){
// Flag requirement at top.
result.top = true;
}
// Check to see if the container bottom is close
// enought (with buffer) to the scroll of the view
// frame to trigger loading more items (at the
// bottom).
if ((containerBottom - bottomScrollBuffer) <= viewBottom){
// Flag requirement at bottom.
result.bottom = true;
}
// Return the requirments for the loading.
return( result );
}
// I check to see if more list items are needed, and, if
// they are, I load them.
function checkListItemContents( container ){
// Check to see if more items need to be loaded at
// the top or the bottom (based purely on position).
// Returns: { top: boolean, bottom: boolean }.
var isMoreLoading = isMoreListItemsNeeded( container );
// Define an onComplete method for the AJAX load that
// will call this method again to make sure there is
// always enough data loaded on the page.
var onComplete = function(){
checkListItemContents( container );
};
// Check to see if more list items are needed at the
// top. If so, we will check to offsets to see if the
// load needs to take place.
//
// NOTE: Position is only *part* of how we determine
// if additional content is needed at the top.
if (
isMoreLoading.top &&
container.data( "minOffset" ) &&
(container.data( "minOffset" ) > 1)
){
// Load and prepend more list items.
getMoreListItems(
container,
"top",
onComplete
);
// Check to see if more list items are needed at the
// bottom. For this, all we are going to rely on is
// the offset of the container (since we can load
// ad-infinitum in the bottom direction).
} else if (isMoreLoading.bottom){
// Load and append more list items.
getMoreListItems(
container,
"bottom",
onComplete
);
}
}
// -------------------------------------------------- //
// -------------------------------------------------- //
// When the DOM is ready, initialize document.
jQuery(function( $ ){
// Get a reference to the list container.
var container = $( "#container" );
// Bind the scroll and resize events to the window.
// Whenever the user scrolls or resizes the window,
// we will need to check to see if more list items
// need to be loaded.
$( window ).bind(
"scroll resize",
function( event ){
// Hand the control-flow off to the method
// that worries about the list content.
checkListItemContents( container );
}
);
// Now that the page is loaded, trigger the "Get"
// method to populate the list with data.
checkListItemContents( container );
});
</script>
</head>
<body>
<h1>
Bidirectional Infinite Scroll With jQuery And AJAX
</h1>
<div id="container">
<!-- Content will be loaded here dynamically. -->
</div>
<!--
This is the tempalte that will be used when adding the
list items to the DOM. It contains two different variable
place-holders:
${src} : The source of the image.
${offset} : The offset of the record.
-->
<script id="list-item-template" type="application/template">
<div class="list-item">
<a href="./${src}" target="_blank" class="thumbnail">
<img src="./thumbs/${src}" width="100" height="150" />
</a>
<div class="details">
<h4>
${src}
</h4>
<p>
Pauline Nordin is a fitness figure professional
athlete, AST sports science spokesperson,
Fitness journalist, fitness model, TV
personality, Trainer and Nutrition coach,
Swedish Bodybuilding champion three years in a
row, Cover model Ironman magazine, Cover model
Body Magazine, fitness profile etc etc.
</p>
</div>
<div class="offset">
Offset: ${offset}
</div>
</div>
</script>
</body>
</html>
<!--- Param the offset variable. --->
<cfparam name="url.offset" type="numeric" default="1" />
<cfparam name="url.count" type="numeric" default="30" />
<!--- Create the return array. --->
<cfset items = [] />
<!---
Loop over the count to populate the return array with
test data. In this case, test data is 1 of 5 randomly
selected thumbnail src values.
NOTE: We are including the offset of each record to
make the Javascript a bit easier (no offset calculations
will be required).
--->
<cfloop
index="index"
from="#url.offset#"
to="#(url.offset + url.count)#"
step="1">
<!--- Create a random item. --->
<cfset item = {
offset = index,
src = "girl#randRange( 1, 5 )#.jpg"
} />
<!--- Append a random image name. --->
<cfset arrayAppend( items, item ) />
</cfloop>
<!--- Serialize the array. --->
<cfset serializedItems = SerializeJSON( items ) />
<!--- Convert it to binary for streaming to client. --->
<cfset binaryItems = toBinary( toBase64( serializedItems ) ) />
<!--- Set the content length. --->
<cfheader
name="content-length"
value="#arrayLen( binaryItems )#"
/>
<!--- Stream binary content back as JSON. --->
<cfcontent
type="application/x-json"
variable="#binaryItems#"
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment