Shhhaw!

Getting Pagination Right with will_paginate and jQuery

12/30/09
JavaScript, Rails, TPM, code

Earlier today I pushed out an update to the TPM PollTracker which, among other small bugfixes, switched all of the pagination over from my previously hacked-together solution to the fantastic will_paginate gem. Will_paginate is stupid simple to use. You just require it and then it adds a paginate method that you can use instead of find. Then you can generate the whole nav link package with one line of code: will_paginate @foo.

The problem is, it’s too easy. As long as you’re fine with the default settings, you can get it up and running in minutes. But if you want to modify even the slightest bit of markup that the will_paginate method generates for you, you need to patch the gem, specifically the WillPaginate::LinkRenderer class.

I needed a little more flexibility, so I patched the class to offer a reverse option. For chronological data like the front page list of polls, and individual contest pages, I wanted navigation to work in “reverse,” e.g.

← Older | Newer →

where you navigate backwards in time, and the “last” page is the oldest. But for alphabetized data like pollster and candidates lists, I wanted conventional left to right pagination, e.g.

← Previous | Next →

where clicking ‘next’ loads subsequent pages.

Before this update, I had all pagination in “reverse,” and a number of people told me this was confusing. Hopefully this dual system makes more sense.

To accomplish this in will_paginate, I needed to create a reverse option that could be passed when generating the nav links.

I did that like this (most of this code is from the original gem, I just added the reverse option):

class WillPaginate::LinkRenderer
        def to_html
            links = @options[:page_links] ? windowed_links : []
            # previous/next buttons
            if @options[:reverse] === true
                links.push page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label])
                links.unshift page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
            else
                links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label])
                links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
            end

            html = links.join(@options[:separator])
            @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
        end 
end

Then in my view, for “reversed” nav packages:

<%= will_paginate @polls, :previous_label => "Newer &raquo;", :next_label => "&laquo Older", :reverse => true, :page_links => false %>

That worked really well. I was able to clear out all my half-baked pagination hacks and streamline how it worked across the site. The next step was to do it in JavaScript, so the “pages” load quickly inline rather than taking you to a new browser page.

To do this, I wrote a jQuery plugin I call ajaxPaginate. The plugin “hijacks” the links generated by will_paginate and uses jQuery’s load() to inject subsequent pages into a specified div on the page. It also creates its own navigation based on hashes rather than query strings. The coolest part is, pagination will work perfectly with JavaScript turned off, and the links will look like ...?page=2. With JavaScript enabled, they will look like ...#page=2. And if someone gives you a hashed link, ajaxPaginate will recognize that, and inject the correct page on the fly.

Another feature— ajaxPaginate will find the computed height of the injected div, in order to avoid collapsing the page while it fetches the new page. During this period, it adds an ajaxLoading class to the div, which I used to show a snazzy spinner.

The code as written is pretty tailored to TPM’s needs (there is a custom callback function after the XHR for creating logged-in elements, for example). At some point, if I can, I may release a clean version of the code for public consumption. Though you’re welcome to give this a whirl as is and let me know how it goes.

To get started, just link ajaxpaginate.js in your page, and then put this init code somewhere below that:

<script type="text/javascript">
        $(document).ready(function() {
            $(this).ajaxPaginate('.your_injected_div',true); 
            //`true` for normal `false` for reverse behavior
        });
</script>

This is still an early sketch, and can certainly be refactored, but hey, it’s my first jQuery plugin.

Late Update: I neglected to mention, you’ll have to patch LinkRenderer’s html_attributes method also. This is to surface data like total_pages as a CSS selector so that jQuery can latch onto it when replacing the page. For reference, here’s my complete will_paginate patch.

Recently

More