#114 Endless Page (revised)
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
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 %>
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
$(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.
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
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) %>');
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()