Rails Pagination by Letters (not Numbers like will_paginate)

I want my list of data to be paginated by letters. The will_paginate plugin certainly gives excellent pagination if all you want is “prev 1 2 3 .. 6 next” kind of pagination.

However what if you’re looking for entries that start with the letter H and you have no idea if that’s page 4 or page 42? You’re probably wanting something more like “# A B C D…” pagination.

I did some googling and found people speaking of solutions for “A B C D…” but in my case, not all of my entries start with letters! If you have something like media titles in your data set, having an entry start with a number is perfectly normal. Some might even start with special characters! Some people suggest having an ‘All’ option, but if you need pagination, it’s probably because you have enough data that showing all options at once is a very bad idea.

Here’s my solution:

First I make a helper function for my options that’ll be cached permanently.

def letter_options
  $letter_options_list ||= ['#'].concat(("A".."Z").to_a)
end

Here’s my index action in my controller:

@letter = params[:letter].blank? ? 'a' : params[:letter]
if params[:letter] == '#'
  @data = Model.find(:all, :conditions => ["title REGEXP ?", 
      "^[^a-z]"], :order => 'title', :select => "id, title")
else
  @data = Model.find(:all, :conditions => ["title LIKE ?", 
      "#{params[:letter]}%"], :order => 'title', :select => "id, title")
end

Here’s my html

<div class ="pagination">
  <% letter_options.each do |letter| %>
    <% if params[:letter] == letter %>
      <span class="current"><%= letter %></span>
    <% else %>
      <%= link_to letter, staff_games_path(:letter => letter)%>
    <% end %>
  <% end %>
</div>

There we go! Now the # will pull up all entries where the first character is not a letter.

14 comments ↓

#1 Brandon on 07.07.08 at 5:46 pm

Bookmarked – that’s helpful.
Thanks for sharing.

#2 Phil Misiowiec on 08.28.08 at 3:13 pm

Very useful, thanks! Also, if you want to filter the paginator to only show letters corresponding to existing records, you can do this:

@letter_options_list = Model.active.collect!{ |c| c.title[0,1].upcase }.uniq!.sort!

#3 Phil Misiowiec on 08.28.08 at 3:14 pm

Note: in the above ‘active’ is a named scope I set up for my ‘model’:

named_scope :active, :conditions => “status != ‘Closed'”

#4 Glenn Ford on 08.28.08 at 3:23 pm

I like that, thanks for the tip Phil!

#5 Phil Misiowiec on 08.28.08 at 4:19 pm

You bet! Above code can be simpliflied it a bit by using the Rails ‘first’ string extension.

@letter_options_list = Model.active.collect!{ |c| c.title.first.upcase }.uniq!.sort!

#6 Nathan on 09.03.08 at 4:44 pm

sorry newbie question…. I get an error when using staff_games_path….why is that? and how do I fix it.

#7 Glenn Ford on 09.03.08 at 5:19 pm

@Nathan: staff_games_path a particular path in my application. Run ‘rake routes’ in your console to see what paths exist in your own application.

#8 Nathan on 09.04.08 at 9:41 am

I just have the following:

/:controller/:action/:id
/:controller/:action/:id.:format

what exactly am I linking to? Is it the current page?

#9 Glenn Ford on 09.04.08 at 9:44 am

Yes it should be the current page. The goal is to reload the same page, only with a different ‘letter’ parameter so that your controller action can load the new data.

#10 Lajuana on 10.27.08 at 1:01 pm

Well said.

#11 lanetrain on 01.04.09 at 4:19 pm

Hi,

May be a bit late but you’re initiating @letter but not using it in the controller. This causes no data if params[:letter] isn’t set. I changed the conditional statements to use @letter instead of params[:letter]

Thanks

#12 lanetrain on 01.04.09 at 4:19 pm

Oops – you should do this in the view too!

#13 Gleb Pereyaslavsky on 09.14.09 at 5:07 pm

Thank you for the code, it appeared to be very useful. I could suggest a slight refactoring, though. The controller’s code,

Model.find( … )

to be changed into model’s named scopes

named_scope :non_alphabetical, {:conditions => [“name REGEXP ?”, “^[^a-z]”], :order => ‘name’}
named_scope :starting_with, lambda{|letter|{:conditions => [“name LIKE ?”, “#{letter}%”], :order => ‘name’}}

and use this way:

if params[:letter] == ‘#’
@data = Model.approved.non_alphabetical
else
@data = Model.approved.starting_with params[:letter]
end

This way it become more readable.

One more thing regarding performance, it would be useful to create an indexed column containing the first letter of the model name, and to make all selects using this column.

Also the block with the list of letters could be moved into partial to be used multiple times (on top and bottom for example).

#14 Gleb Pereyaslavsky on 09.14.09 at 5:10 pm

Model.approved is my another named_scope, so just ignore it.