One of the really cool things I like about Bing is its image search – you can search on something and keep scrolling forever without once hitting the annoying “next page >>” fail pattern.
Why do I call this a “fail pattern”? Because we know better by now – we know that people rarely go to pages 2- whatever and that when they do, they rarely come back to page 1. We have 3 seconds to capture a user’s attention on a new visit – that’s it. If you make them page they will leave, and it’s amazing how many applications still use this dated way of showing information.
Peeps – there’s a better way: “Bottomless” scrolling and as you might have guessed by now, it involves jQuery.
Preface
Imagine you want to buy a groovy gadget and you go to Gadget Outlet that was written in 1999 using Classic [Web Platform]. Usability didn’t really matter back then and you find the search functionality it basically a “LIKE %blah%” – sending you into a paging nightmare as you click “Next Page >>” forever to find your gadget.
We don’t need to build things this way anymore – it’s not optimal and is a pain. You lose context almost immediately and the user can’t “go back” to see what they’ve been looking at. In other words the experience is jilted and linear.
The good news is it’s pretty simple to create a bottomless scroller using jQuery and your favorite web platform – for this I’ll be using ASP.NET MVC, but you can translate as needed…
The Setup
The first thing we need to do is to figure out when a user scrolls to the bottom of our list – we’re going to want to fire an AJAX query back to our server to dynamically show more “stuff” when they get there. I nabbed the basics of this script online doing a simple Google search:
$(window).scroll(function() { if ($(window).scrollTop() == $(document).height() - $(window).height()) {loadMore();
}
});
With this code we’re simply wiring the browser’s scroll functionality to call another function called “loadMore()” if the top of the scrolled window is the bottom of the page. I’m sure this could be written better…
The Data
I’m using my blog (the very one you’re reading here) as my test ground – it’s why I made it – and what I want to do is to show an “archive” of posts that users can look over, ordered by descending publish date.
To keep the page nimble, I only want to query for 20 records at a time using “server-side” paging – meaning that want the database to decide the records to return so I don’t query the whole bunch and slice them up in memory. Doing it that way would be costly in terms of performance.
Using ASP.NET MVC I’ve created an ArchiveController with an Index action that does exactly this, with SubSonic’s help:
const int PAGE_SIZE = 20;
public ActionResult Index(int? id)
{var pageIndex = id.HasValue ? id.Value : 0;
var skipPosts = PAGE_SIZE * pageIndex;
//pull back the last 20 posts, pagedvar posts = Post.All()
.OrderByDescending(x => x.PublishedOn)
.Skip(skipPosts).Take(PAGE_SIZE);
var model = new ArchiveViewModel(posts); return View(model);}
I’m passing in an optional argument that indicates the “page” that the user wants. In a URL this would look like this: http://blog.wekeroad.com/archive/index/1
This works great for the page itself, but it’s not setup for AJAX calls just yet. Every request sent to this Controller will return the full HTML of the page – what I want is a simple list of post summaries and to optionally return those if the call is Ajax-based.
To handle this I create a “partial” – which you can think of as a UserControl in WebForms terms – and it simply shows a list of post summaries. I’ll call this “PostSummaryList”:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<PostView>" %><%foreach (PostView post in Model){%>
<div class="column">
<h2>
<a href="<%=Url.Action(
"Details","Posts",
new {year=Model.PublishedAt.Year,
month=Model.PublishedAt.Month.FormattedDayMonth(),
day=Model.PublishedAt.Day.FormattedDayMonth(),
id=Model.Slug
})
%>"
rel="bookmark" title="Permanent Link to <%=Model.Title %>!"
class="title"><%=Model.Title%></a></h2>
<p style="margin-bottom:10px"> <b><%=Model.PublishedAt.ToLongDateString()%></b> - <%
=Model.Summary%></p>
</div>
<div class="column span-15" style="text-align:center;margin-bottom:16px">
<img src="/content/images/plum_separator.png" />
</div>
<%}%>
Now I can update my ArchiveController to return this partial if the request is an Ajax request by using the “Request.IsAjaxRequest()” method on the Controller. This will read the headers and looks for a value called “x-requested-by” which both jQuery and MS Ajax libraries send along. If the request is an Ajax one, I return the partial as the View. If not, I return the full page:
const int PAGE_SIZE = 20;
public ActionResult Index(int? id)
{var pageIndex = id.HasValue ? id.Value : 0;
var skipPosts = PAGE_SIZE * pageIndex;
//pull back the last 20 posts, pagedvar posts = Post.All()
.OrderByDescending(x => x.PublishedOn)
.Skip(skipPosts).Take(PAGE_SIZE);
var model = new ArchiveViewModel(posts); if (Request.IsAjaxRequest()) {return View("PostSummaryList", model.PostList);
}
return View(model);}
The Client
Now all I need to do is to wire up some jQuery love on the client and we’re good to go. The first consideration is a visual “cue” that something’s happening – for that I’ll use the standard “loading” icon in place of where the new stuff will show up.
Next I need to take the HTML returned from the callback and append it. Fortunately all of this is trivial with jQuery:
<script src="/scripts/jquery-1.3.2.min.js" type="text/javascript"></script>
<script src="/scripts/jquery-ui-1.7.2.custom.min.js" type="text/javascript"></script>
<script type="text/javascript"> $(window).scroll(function() { if ($(window).scrollTop() == $(document).height() - $(window).height()) {loadMore();
}
});
var current=0;function loadMore() { if (current > -1) {current++;
$('#loading').html("<img src='/content/images/bigloader.gif' />");
$.get("/archive/index/" + current, function(data) {if (data != '') {
$('#results').append(data); $('#loading').empty(); } else {current = -1;
$('#loading').html("<h3><i>-- No more results -- </i></h3>");
}
});
}
}
</script>
I know I can write this better – if you have some suggestions please do leave them below… I’ll add them to my page.
In the above code I’ve set a “kill switch” that monitors the return from the Controller since we don’t want the callback to happen endlessly. If the return is an empty string, I simply set the “current” variable to –1 and check it before making any further calls.
This variable (“current”) increments the page calls for me when a user scrolls down – this is the thing I pass to my ArchiveController so it knows which data to grab.
I then use jQuery’s “get()” function, which simply makes a call to my controller and I handle the callback inline – using “append()” to append the returned HTML into a tag with the ID of “results”.
Finally, I flash the loading image by injecting the HTML when “loadMore()” function is called. I tried to do this setting visibility with “toggle()” and “show/hide”, but this is the only way I could get it to work.
Demo
Want to see this stuff live? Well have a look – it’s why I built this blog
. You can also use it to browse post categories from the home page.
