Ajax Pagination in less than 5 minutes
Written by matt on September 26th, 2007
updated Nov 26 to reflect the recent low pro changes. (please use low pro 0.5 and Prototype 1.6)
Recently one of my client asked me to add 'ajax' pagination to his application. His site already had a very nice pagination using the excellent will_paginate from Mislav and the guys(PJ & Chris) from err the blog but since my client had a special need where he had to have Ajax.
It took me virtually no time to convert the standard pagination into an Ajax navigation while still degrading gracefully.(it works even without Javascript)
I really enjoy using will_paginate, it's very well written and the authors keep up with the bugs and new features.
Start by installing will_paginate:
ruby script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
Then go watch the Railcast screencast about will paginate.
Once you have your pagination working, we will do some 'progressive enhancement'.
What we want is to add a behavior to the pagination link. The behavior would make the same call than the normal link but via an ajax(Javascript) call.
Add lowpro
To do that, you simply need to add the excellent 'lowpro' Prototype extension from Dan Webb
You can get the files directly from the lowpro's repository.
Add lowpro.js to your public/javascript folder.
Don't forget to include the javascript in your page. (
1 2 |
<%= javascript_include_tag 'lowpro' %> |
Create a behavior
Now open your application.js file (or whichever Javascript file you're using) and add the following:
1 2 3 4 5 |
Event.addBehavior.reassignAfterAjax = true; Event.addBehavior({ 'div.pagination a' : Remote.Link }) |
Refresh your cache, reload your page, and test the link. It will probably look like it doesn't do anything but if you are using firebug or if you are checking your logs, you'll notice something happened. The problem is that we didn't tell our action to send an Ajax response so we get the html full page all over again.
Setup a response for javascript requests
Got to your action handling the pagination and let's setup a response for Javascript:
1 2 3 4 5 6 7 8 9 10 11 12 |
def index @photos = Photo.paginate(:all, :conditions => ["photos.user_id = ?", current_user.id], :page => params[:page]) respond_to do |format| format.html # index.html.erb format.js do render :update do |page| page.replace_html 'photos', :partial => "photos" end end end end |
Perfect! Now when a visitor clicks on my pagination links, only the photos are paginated, the rest of the page stays the same. Note that my navigation bar is inside the partial so it gets 'updated' after a visitor clicks on any pagination link.
Read more and convert to UJS (unobtrusive javascript)
Read more about UJS and think about replacing all your nasty inline javascript snippets by pretty behaviors :) (Think about stopping using the obtrusive rails helpers)
Comments
-
The partial that is returned has an updated pagination area also, if I read what you wrote correctly. Does low-pro pick up those links also, so that they will be remote ajax calls?
-
@Jacob, my pagination links are only in the partial itself. At the bottom of the partial I'm calling
<%= will_paginate @photos %>Lowpro picks up these links too, even after the visitor clicked on a pagination link and reloaded the pictures.
-
This is the first Ajax Pagination post I've ever seen! Really simple and functional! Congratulations!
Felipe ;-)
-
I just thought I'd point this out, in case anybody runs into an error. If you are using
javascriptincludetags :defaults, you will actually need to break it out into individual calls to each file. This is to ensure that you load the files in the proper order:- prototype.js
- lowpro.js
- remote.js
- application.js
Loading the javascript files in this order will satisfy the dependencies correctly.
Great post, Matt!
-
Encountered a weird problem. May be you guys could point me in the right direction. Tried following the instruction above. Setup the necessary javascript call (using lowpro) in my application.js. I'm pretty sure that when I click the pagination button an AJAX call is being thrown. Traced it using firebug. In the controller where the pagination is being handled the handling of the javascript is not being process. The "format.js do..." block is not being executed. Any chance some of you guys could shed some light on this. Thanks.
-
What's the Ajax response? Firebug should give you a response. Is it the full html page? make sure your respond_to block works properly. I'm running my apps on rails 2.0 so I can't guarantee it works with 1.2.4.
-Matt
-
hi. i was wondering how i can use this in an action that calls a form? paginate will recall that form action.
i have this
def action initialize my form data render my custom template ajax pagination code.... end
i am basically trying to create a blog post form, but also list photos so the user can have the photos addresses handy. id like to be able to paginate these photos on the form page. kind of in a slump.
thanks!
-
Mislav is working on that. I personally patched the source until Mislav is done, check his progress there: http://err.lighthouseapp.com/projects/466-plugins/tickets?q=tagged%3Awill_paginate
-Matt
-
I've tried this with Rails 2.0, and it doesn't seem to be doing anything. The result still refreshes the page and I'm using the newest versions of all of the libraries.
-
Robert, if the whole page is refreshed that means that the remote call didn't work. Use Firebug to see if an Ajax call is made. If it isn't that probably means that your Javascript Behavior isn't set properly or that your js file isn't loaded properly or that the remote file is missing.
Good luck.
-Matt
-
My application.js has: Event.addBehavior({ 'div.pagination a' : Remote.Link });
My view rendered looks like: ... <head> <title>Locations</title> <link href="/stylesheets/main.css?1191535166" />
</head> ... some more stuff ...
...
Reponds statement in the index method respond_to do |format| format.html # index.html.erb format.js do render :update do |page| page.replace_html 'table', :partial => "table" end end end
I'm using Rails 2.0, with prototype 1.6.0rc1 and the newest willpaginate from the repository.
This seems to be exactly what the article says. Where could I be going wrong?
Thanks.
Rob
My application.js has: Event.addBehavior({ 'div.pagination a' : Remote.Link });
My view rendered looks like: ... <head> <title>Locations</title> <link href="/stylesheets/main.css?1191535166" />
</head> ... some more stuff ...
...
Reponds statement in the index method respond_to do |format| format.html # index.html.erb format.js do render :update do |page| page.replace_html 'table', :partial => "table" end end end
I'm using Rails 2.0, with prototype 1.6.0rc1 and the newest willpaginate from the repository.
This seems to be exactly what the article says. Where could I be going wrong?
Thanks.
Rob
Another thing to mention is I tried using Firebug to set a breakpoint in application.js on the addBehavior line, as well as other places in remote and lowpro, and nothing hit. There is some very obscure bug.
Matt,
This works like a charm with trunk lowpro and new 1.6 prototype. I got everything working perfectly except for one small detail: After you "update" the first time, the lowpro behavior does not seem to be attached, thus falling back into a non-ajax call. Is this due to the trunk lowpro / 1.6 prototype combo, or do you have any idea why that may be happening?
Best
@ Malko
Low pro 0.5 changed a bit and you now need to set reassignafterAjax to true.
Event.addBehavior.reassignAfterAjax = true;
Thanks for the comment, I forgot to post about this recent change.
hi, nice article. unfortunately the link to 'remote.js' is not working for me at the moment, getting a 404: http://svn.danwebb.net/external/lowpro/trunk/behaviours/remote.js
@bob
remote.js has been merged into the lowpro.js base. This is why you can't find it. If you just download the new lowpro (which works with prototype 1.6), you should be fine. Just make you see Matt's comment above.
Best
Hi,
This seems like a fantastic way to paginate, I already had the "will_paginate" plugin installed, so I just wanted to implement that lowpro feature. I have the latest prototype & lowpro included in my page, checked that the files were in the correct path and readable.
but when I reload the page, all I see in firebug is an error: Remote is not defined [Break on this error] 'div.pagination a' : Remote.Link
been through the lowpro.js file, and found this function in line 275 "Remote.Link = Behavior.create(Remote.Base, { ..."
so I'm stucked... :( anyone has a clue of what I did wrong ?
thanks
julien
oooops, a bit of logic and I found it....
using javascriptincludetag :default the application.js was loaded before lowpro.js, it's why I had the error.
I replaced the default tag to include the javascripts in correct order : prototype, lowpro, application and now it works :)
Just updated the article to reflect the latest low pro changes.
A quick trick, if you don't want to use Event.addBehavior.reassignAfterAjax = true; because it might slow down your application, you can always use
Event.addBehavior.reload();
to manually reload the behavior rules.
Hi Matt,
I just tried your update, and using low pro 0.5 and Prototype 1.6.0, things seem to work properly during the first ajax click, but subsequent ones cause two events to fire. Do you know what's up with that?
hi, i'm trying to use this example, i think i've done all right but my partial is update with this code: try { Element.update("components", "\u003Cdiv id=\"components\"\u003E\n\n \n\u003Ch2\u003EDimmer\u003C\/h2\u003E\n\n\u003Ca href=\"\/components\/new_component\/5\"\u003ENew 'Dimmer' Component\u003C\/a\u003E\u003Cbr \/\u003E\u003Cbr \/\u003E\n\n\u003Ctable cellpadding=\"0\" cellspacing=\"0\" class=\"border\"\u003E\n \u003Cthead\u003E\n \u003Ctr\u003E\n \n \u003Cth\u003EName\u003C\/th\u003
instead of the correctly formatted html. What am i doing wrong?
hello,
I'm having the same problem as juju:
Firefox says Remote is not defined 'div.pagination a' : Remote.Link.
I'm sure that I don't have any call to javascriptincludetag : default
And in my application.rhtml I have:
I don't see in the debuger any error for loading this files.
And in my application.js I have:
Event.addBehavior.reassignAfterAjax = true; Event.addBehavior({ 'div.pagination a' : Remote.Link })
Any clue ?
thanks,
rai
@rai make sure you are using the latest version of lowpro and that lowpro is loaded before application.js
I'm sorry I can't help more :(
This works well when you only have one paginator per action. But will_paginate builds the links from the template params so I can't figure out how to have multiple paginators on a single page. Does anyone have some pointers to get that to work? I was thinking along the lines of using different class names for each pagination div and adding different Behaviors but I'm not sure how to modify Remote.Link to call out different actions than the default.
I'm running into a weird error where the ajax call seems to be working however instead of replacing the partial it is putting in a new copy on top of it shifting it down and to the right. I can still see the top border of the previous page, and like I said earlier all the new pages show up shifted slightly, down and to the right?
Any ideas on what may be causing this and how I may be able to resolve the issue?
@francesco
You're probably using an :update=>'elemname' in your remotefunction which causes the element to be replaced with the jscript returned by the render :update {...}
@jake
I'm having the same issue has francesco, but it only occurs after you flip through the pagination, click on a new page and then hit the back button to return to the page with the pagination on it.
I'm kind of new at this so where might I find that remote function that you mentioned? I took my code straight from the example above. And while this error might be something some people may never see it would be bad for the people that do run across it.
Any help would be appreciated. Thanks.
rai and others - I just had a similar problem and it is due to prototype 1.5.0. Upgrade you're prototype.js and you'll be much better.
@Tom
I got multiple element ajax pagination working fairly easily.
- in your controller make sure you have unique page params defined when using paginate :page => params[:page_assets] :page => params[:page_comments] etc...
- Also in the controller check what page param has been submitted and act accordingly
if params[:page_assets] respond_to do |format| format.html format.js do render :update do |page| page.replacehtml 'assetlist', :partial => "assets/show_assets", :locals => {:assets => @assets} end end end end
Probably could move this into a view helper to minimize duplication when checking multiple conditions but i couldn't be bothered at the moment.
Using this method how could I add an effect to this? Maybe one of the script.aculo.us effects.
Matt
Thanks for figuring out the issue with :defaults. Pulling my hair out, until I read your comment.
Cheers,
GP
Hmm, I'm having a really simple problem; pagination is working, the ajax calls are updating the content however the page numbers aren't posting pack so that the correct page is "highlighted" as being the active.
If I do the URL
items?page=4
that obviously shows the correct page number as being selected.
Ok, I got it, didn't see the note regarding putting the pagination tags inside the partial, wow... it finally worked!!!!
Nice pagination enhancement. Got it working mostly except it seems every other page submit executes a browser refresh with URL update to my browser path bar. So to be a little more clear click from page 1 -> 2 goes fine, 2 to 3 causes a refresh, from 3 back to 1 causes no refresh. Seems it's every other pagination click that causes a refresh no matter how many pages or what order I click the pages in. Anyone have any clue on this? I'm Using rails 2.0.2 and latest prototype.
Let me just say: when the title says '5 minutes' it means 5 minutes!!! if that!! :)
Great tutorial. Worked perfectly, first try. And it helped me wrap my brain around UJS/LowPro a little more. Beautiful!
So, here is an issue. This all works well, but let's say I'm using it in a scenario where I have multiple tabs that are also loaded via ajax. Each tab displays different data of the same model. For instance let's say the model is Post, and I have tab links to posts by category, author, rating and user favorites. I used a different action in the controller for each of these tabs.
What happens with the pagination is that it works fine for the default /posts view. However, when I get to the highest rated tab, the records load and the paginating links show up but when I click the link, it doesn't go to the next page. I can see the request in the console, it's requesting /posts/highest_rated?page=2 but that action isn't returning to the view like the default posts is.
Any ideas?
Brodie: Maybe you're using different containers for the different tabs? In this case, you'd have to customize some of Matt's stuff. If Firebug executes the AJAX request correctly, it's usually a sign that there's something wrong with response evaluation.


