All the pages, linked everywhere

The Problem (AKA “design”)

In one project (that’s not live yet) a designer has shown a pretty original approach to displaying pagination links and client was immediately convinced that’s the way to go.

The idea was: we present first and last pages as standard links, but between them — instead of the standard three-dot — we put a select with all the pages “between”, of course doing a javascript-powered redirect to the chosen page. Like this:

In terms of business requirements, if the page count is equal to or below 10, we show just the links to the pages. If there are more than 10 pages, we put 5 to the left, 5 to the right and all the excess pages are accessed via a select in the middle. The redirect occurs automatically on select. The current page, if it’s amongst the options in select, is displayed in bold.

The Approach

From the design it seems we need a constant amount of page links on both “sides” of the pagination helper. And (this is an important part of the approach) we need to generate the select in place of gap.

Dive In

That’s where mislav-will_paginate really shines. It can use custom renderers — classes (I put them in app/helpers directory) extending WillPaginate::LinkRenderer with a few methods to (re)implement: in this case we’re going to implement prepare (for assigning our custom gap) and visible_page_numbers (so we always have links on both sides to first and last pages and no links around the gap).

Let’s make it flexible enough so the number “5” (amount of page links to the left and right of select) isn’t hardcoded, just use well-known outer_window option.

In our application we had to reimplement also the to_html method to have some additional fancy stuff on the edges, but that’s not relevant.

Now I know it’s not a very preferred way of learning, but let’s see the code that does the trick:

classFancyRenderer<WillPaginate::LinkRendererdefprepare(collection,options,template)@gap_marker=options.delete(:gap)superenddefvisible_page_numbersinner_window,outer_window=@options[:inner_window].to_i,@options[:outer_window].to_iwindow_from=current_page-inner_windowwindow_to=current_page+inner_window# adjust lower or upper limit if other is out of boundsifwindow_to>total_pageswindow_from-=window_to-total_pageswindow_to=total_pagesendifwindow_from<1window_to+=1-window_fromwindow_from=1window_to=total_pagesifwindow_to>total_pagesendvisible=(1..total_pages).to_a#start with all of them...left_gap=right_gap=(2+outer_window)...(total_pages-outer_window)visible-=left_gap.to_aifleft_gap.last-left_gap.first>1visible-=right_gap.to_aifright_gap.last-right_gap.first>1visibleendend

It’s pretty obvious from the code (don’t worry, it took me some time to write it) that mislav-will_paginate rendering uses @gap_marker variable and we need to call will_paginate helper with :gap option. If we don’t give it

It’s time to write the code that’ll create the select for remaining “between” pages!

We’re not there yet

You know what I like most about Rails? That it’s written in Ruby. And in Ruby you can really do some fancy shit that’s readable and usable at the same time.

We’re going to write a “gap select” generator that’s going to create a required amount of relevant select options with paginated paths as values. But we want to make the code as elasic and reusable as possible (DRY, motherfucker! Do you follow it?), so calling the given resource path generator within our gap-generating helper is not an option.

The “stripped_params” are here of course for the sake of including all the other GET parameters (see “links for extra parameters” section in my post about Searchlogic), made by cloning params and removing key/value pairs that could interfere with path generator.

So, we have sexy, DRY and cool code ready to reuse. Let’s write the page_selector_generator helper (finally!) to feed it with these stripped_params and path helper block.

moduleApplicationHelper#accepts a block (one argument: page) with a path-generating helperdefpage_selector_generator(collection,extra={},first='jump to',&block)arr=Array.newarr<<[first,'']offset=collection.total_pages-6(5..offset).eachdo|page|page+=1arr<<[page,block.call(extra.merge({:page=>page}))]endcontent_tag:select,:id=>'page_selector'do#the second argument is here for giving current page a "selected" attributeoptions_for_select(arr,block.call(extra.merge({:page=>collection.current_page})))endendend

And it looks this way:

So… voila!

(Yeah, I know, the “5” left-and-right offset is hardcoded, so I’ll probably refactor this code soon)

Almost there, i.e. proofing the awesome

The above code is great with one exception: if you supply some extra parameters, the path in select option’s value is not generated properly — the ampersands (&) get escaped and thus the paths from value don’t yield what would be expected of them.

The above code may be ugly (I never really digged html-generating helpers), but it does the trick: the paths are not entity-escaped and we can finally move onto javascript redirects.

I wanted to end it with “this remains as an exercise for the reader”, but I just got reminded how pissed I always was when encountering such crap: it’s a tech blog article, goddamit. People come here for answers, not for fucking riddles.

Of course we’ll write the javascript the unobtrusive way. Here’s the snippet if you’re using Prototype:

Searchlogic + Mislav_Will-Paginate = WIN

Old School

If you’re using Searchlogic 1.6.x, you can easily prepare instance variable in a way digestible for will_paginate helper:

search=Model.new_search(...)# searchlogic scope preparation # remember to also do the pagination, i.e. make use of :page and :per_page parametersmodels_arr=search.find(:all)# array of elements for current page@models=WillPaginate::Collection.new(search.page,search.per_page)@models.replace(models_arr)@models.total_entries=search.count

And you’re ready to use will_paginage(@models) in your views!

Searchlogic 2.x

New searchlogic exposes “all” method’s result as named scope (and thus works great in named scope chains), so it’s even easier to make it work with model methods added by mislav-will_paginate: