#114 Endless Page (revised)
- Download:
- source codeProject Files in Zip (86.7 KB)
- mp4Full Size H.264 Video (17.9 MB)
- m4vSmaller H.264 Video (9.39 MB)
- webmFull Size VP8 Video (10.7 MB)
- ogvFull Size Theora Video (20.9 MB)
Below is a page that shows a list of products. It currently uses pagination to enable users to browse between pages but we’d like to replace this with endless scrolling so that as a user scrolls to the bottom of the page the next set of products is fetched automatically.
We’re using will_paginate to do the pagination, which we covered back in episode 51, although we could just as easily have used Kaminari. In the ProductsController
’s index
action we fetch the products for the given page ten at a time.
def index @products = Product.order("name").page(params[:page]).per_page(10) end
The template renders the products through a _products
partial and shows the pagination links at the bottom of the page.
<h1>Products</h1> <div id="products"> <%= render @products %> </div> <%= will_paginate @products %>
We’re not going to change any of this existing code to add the endless scrolling, we’ll add it all through JavaScript. This way the page will fall back to the traditional pagination behaviour when JavaScript is disabled. There are many different ways to add endless scrolling. The one we’ve chosen is the simplest solution we’ve found and doesn’t require any external plugins. We’ll write the code in the products
CoffeeScript file and we’ll start by adding some code that will be triggered when the user scrolls near the bottom of the page.
jQuery -> $(window).scroll -> if $(window).scrollTop() > $(document).height() - $(window).height() - 50 alert('near bottom')
Once the DOM has loaded we listen to the scroll
event on the window and when the user scrolls the browser window the we detect how close to the bottom of the page they are. $(window).scrollTop()
tells us how far from the top of the page the user has scrolled. If this value is greater than $(document).height()
minus $(window).height()
then the user is past the bottom of the page so we also subtract a buffer so that the code is triggered when they scroll to within 50 pixels of the bottom of the page. For now we just show an alert
when this happens.
An alternative solution is to use the jQuery Waypoints plugin to monitor when the pagination element enters the window, but our solution is perfectly adequate for our simple page. If we reload our page now and scroll towards the bottom the alert will be shown so our code is working as it should be so far.
Adding Results
Next we’ll modify our code so that it adds more products when the user scrolls near to the bottom of the page and we’ll replace with the alert
with some code to do that. We can use jQuery’s getScript
function to do this but which URL should it call? The code needs to fetch the next page of results and that link is already available on the page in the pagination so we can reuse that here. The next page in the pagination will have a class of next_page
and we can use that to get the correct URL. (If you’re using Kaminari rather than will_paginate note that these names will be different.)
jQuery -> $(window).scroll -> if $(window).scrollTop() > $(document).height() - $(window).height() - 50 $.getScript($('.pagination .next_page').attr('href'))
This code triggers the ProductController
’s index
action, passing in the correct page, and expects some JavaScript to be returned. The action doesn’t currently respond to JavaScript so we’ll need to add the relevant template. We’ll use a js.erb
template here. We could use js.coffee
and which allows us to execute Erb in CoffeeScript but that’s not necessary for our simple script.
$('#products').append('<%= j render(@products %>)'); $('.pagination').replaceWith('<%= j will_paginate(@products) %>');
The first line of this page renders the next page of products and appends them to the div that contains the list of products. As the products are rendered out to a JavaScript string we use the j
method to safely escape this. We also update the pagination control as our endless scrolling depends on it having the correct page selected. We do this by calling will_paginate
for the new page of products and replacing the current pagination control with this.
If we test this in the browser now we do see more products as we scroll down the page, but more products than we want are fetched at once.
We can see why by looking at the development console. When we scrolled down to the bottom of the page the code that fetches more products was called a number of times when we only want it to be called once. The problem is that while we fetch more products the scroll event continues to be fired and fetches products over and over again. To prevent this from happening we need to stop the scroll
event from firing while we’re fetching more products. Here’s our solution.
jQuery -> $(window).scroll -> url = $('.pagination .next_page').attr('href') if url && $(window).scrollTop() > $(document).height() - $(window).height() - 50 $('.pagination').text('Fetching more products...') $.getScript(url)
While we’re fetching more products we now replace the pagination HTML with some text. This means that until the next set has been rendered the link for the next page of products won’t exist on the page and we can check for that before fetching any more products. Once the next page of results has been rendered and the user scrolls down to the bottom of the page the URL will be available again and we can fetch the next page of products when the user scrolls to the bottom of the page again.
We can try our page again now and when we scroll to the bottom of the page the code to fetch more products is run only once, just as we want.
There’s still one small issue. When we scroll down until we’ve displayed all the products the pagination control shows at the bottom of the page. We can’t remove it from the page as our endless scrolling depends on it so let’s hide it instead.
We can fix this in the index.js.erb
file. Instead of always replacing the pagination we’ll only do so if there’s another page of products still to display.
$('#products').append('<%= j render(@products) %>'); <% if @products.next_page %> $('.pagination').replaceWith('<%= j will_paginate(@products) %>'); <% else %> $('.pagination').remove(); <% end %>
Now when we scroll down to the bottom of the list of products the pagination control doesn’t show on the page.
Our endless scrolling behaviour is now almost complete but there are a couple of loose ends that we should tie up. One good practice is to trigger the window’s scroll
event when the page first loads. If the user’s browser window is tall enough to display all of the first page the second page of products will then automatically be loaded. We should also check that we’re on the correct page before we start listening to the scroll
event, otherwise this code will listen for that event on every page under the ProductsController
. We can do this by checking that the pagination control exists on the page before listening to the scroll event. With these in place the final version of the code looks like this.
jQuery -> if $('.pagination').length $(window).scroll -> url = $('.pagination .next_page').attr('href') if url && $(window).scrollTop() > $(document).height() - $(window).height() - 50 $('.pagination').text('Fetching more products...') $.getScript(url) $(window).scroll()