I've added a few books into our bookstore application, and the list is starting to get a little bit long, so we're going to talk about how to add pagination manually and using the will_paginate gem, and to start things off, we're going to jump into the rails console and take a look at the queries that we can do to pull out these pages. By default, we're using
Booked.all, and this pulls out every single book out of the database, and this is fine. However it isn't going to be very efficient, and what we need to use is the SQL offset and limit methods to allow us to grab every page, and then limit those pages to a certain number of results. So let's take a look at limit real quick. If we say
Book.limit(2), this result list is only 2 books rather than all of them. If we say 4, we get 4 books, and so on. So this means that we can set a variable in our application that says it will always return a certain number of results. Now, let's imagine we're just going to use two books per page. How we get the second page is a little bit interesting, so if we want to get the second page of two books per page, then we have to have an offset. So if we want to skip the first two books and get the second page, we can do that, and we can get books three and four as we expected. If we skip the first four books we get the third page and we get books five and six. And lastly, we can get our last page if we skip the first six books. And that, right there is the last book in the database. So this is sort of the concept, for us to skip books and get the page of results, and this is the most efficient way we can do it in SQL, but we have to calculate this number for the offset, and our offset is actually the page number minus one times the number of results per page. So imagine that we have this one right here. So we have
Book.limit(2).offset(4), what this means is: Skip the first four books, and get the next two books. So this is actually the third page. Now, this gets a little bit confusing because we have to calculate how many books we want to skip, and if we skip these books, we can say that we're on the third page. So we want to skip the first two pages, so the first two pages worth of results is actually 2 * 2 which is two pages and two books per page. We want to skip four, which is how we got this offset number 4. You need to do this calculation dynamically everytime that you have a user click on a page because you need to know: Page 10, we need to skip over nine pages worth of books. That gets a little bit confusing, which is why the will_paginate gem is fantastic for abstracting all of this math out for you, and while it's not very hard, you do have to think about it, and you don't really want to. Your application should just magically work, and no one should ever have to go tweak this unless they really need to. So let's talk about the will_paginate gem.
will_paginate is written by mislav, and he's done a fantastic job of it. The usage for this is extremely simple, which is why I recommend it, there's almost only two lines that you need to put into your application, and it magically works. So let's take a look at adding this.
As always: rubygems, grab the will_paginate line here, and we'll open up MacVim and jump into our Gemfile. And we'll go down here and paste it in, run
bundle install and we'll restart our rails server. And now if you take a look at this, what it says to do is to add a paginate method into your controller that queries for this. So for this page, we're on the index action of the books controller, and we'll want to open that up. So if we open up the books controller, rather than doing
Books.all, we can get rid of that, and say
Books.paginate and tell it the page that we're on. If there is no page, it knows automatically that we're on page number one, which is great. We don't have to handle that or anything. This will go and refresh the page and we'll see that nothing changed. Now that's fine, this is working still, it didn't crash which is great, but if we go in here, and we change the per_page to two, we can see that only two results show up. So this did the limit of two, and I think the default is like 25, it might be 30, you can change it globally, you can change it per model, it allows you to do all of that. So you don't have to do math, you can just tell it what you want. So if we go in here and we put in the "page=2", then we see that we get the second page of results. This is a params that we're putting directly into the paginate method. This is what is going into that. You can do this, and you've got your query working properly, all of these pages work, but there's something missing. Where are the links? We haven't added those, and those are where it gets a little more complex to do manually because you have to calculate the total number of pages that are there, and then you have to go through and display a bunch of links, and next and previous. And then you have to calculate which page each one of those links to, and will_paginate does all of that for you with this really really nice little method called will_paginate. So you can just pop this in your view, and you can go into that and paste it in.
You change post to whatever your model is, and if you go back, you have pages, and it knows that there's only four pages becuase if you didn't know that, you could have a fifth page and no results. It doens't really make any sense to have that printed out here. So this does the math and figures out how many pages there should be. We can jump throgh all of these pages we can't go directly to a page, we can g o next, previous, and that's really cool. So this gem handles a lot of the complexity, and you have to do almost nothing to add it into your application. The beauty of this, is that all of the complexity of limits, and offsets and generating links is handled externally for you and there's no changes that you need to make. Other than telling it how many you want per page. That is really well designed, and that's why you want to write your code whar it's just accepted that this works, and no one has to question how it works. You never get presented with like: Well how does that work? You just know that it's going to paginate my results, and it's going to put links up there. That is a sign of really well written gem.
One last thing before we go, there is will_paginate-bootstrap which allows us to integrate this a little bit better with bootstrap. So the rubygems results, run
bundle install, restart our rails server, and let's hop into the documentation, take a look at how we need to integrate that. So it makes a pretty bootstrap version of will_paginate results, and it looks like all we have to add is the render into our view. So if we go into books/index and we paste in render bootstrap pagination rails, refresh our application and automatically we have prettier navigation for our pagination. And that's all for this episode. (Peace )
Transcript written by Miguel
That was a great screencast! Keep up the good work. I was just wondering if there's a way to limit users from accessing pages without any posts? Maybe by editing routes.rb?
You could probably do something like this in your controller:
@posts = Post.paginate(page: params[:page])
redirect_to root_path if @posts.empty?
If you are a noob like me and it doesn't work try adding ', :per_page => 1' as your controller pagination option.
I was working with seed data with only 10 customers in it so will_paginate was not rendering because the default 30 meant there was no reason to render it at all! I swore I had some bug but turns out it was just that.
Great idea! I've done that in the past when I wasn't sure why it wasn't rendering and that helped a lot. :)
I have a multi page group member list list with a drop down selection box where one can edit a membership role, which is tied to an enum value.
It works, but in the case where one make edits on 2 or more separate pages and then updates, only the edit(s) on the current page are saved.
Is this the expected behavior? It obviously wipes out attributes when pagination occurs, but it does not seem intuitive to expect the user to update between switching pages.
... or maybe I'm just abusing will_paginates' purpose.