- Read Tutorial
What Kaminari Provides
Kaminari is Japanese for thunder and lighting and this gem provides a powerful punch with an elegant implementation for pagination in Rails applications. Many gem developers choose to incorporate the Kaminari gem as a dependency as opposed to the popular will_paginate gem due to the simplicity of the design and how much easier the AJAX implementation is to implement. In fact, popular gems such as Rails Admin and Smart Listing both use Kaminari to manage pagination.
The features of Kaminari are:
Pagination
Straightforward AJAX implementation
Multiple style options
Drawbacks
As I mentioned in the will_paginate gem walkthrough, the key drawback I've run into has been that Kaminari
has namespace conflicts with will_paginate
. I've created these initializers for anytime where I need to resolve the conflicts: Kaminari / Will Paginate. If you add these to your config/initializers
directory both gems can work properly in the same application.
Implementation / Kaminari Example
If you went through the will_paginate
walkthrough you'll notice that the basic Kaminari
setup is very similar, let's start by creating a new application:
rails new kaminari-tutorial -T
After running rake db:create && rake db:migrate
we can add the gem to the Gemfile
:
gem 'kaminari', '~> 0.16.3'
After running bundle
we have what we need to start the implementation. Let's create a scaffold for posts
:
rails g scaffold Post title:string author:string
Run rake db:migrate
and open up the new posts
controller file and update the index action:
# app/controllers/posts_controller.rb def index @posts = Post.page(params[:page]) end
That's it! Now open up the index
view template and add the view helper towards the bottom of the file:
<p id="notice"><%= notice %></p> <h1>Listing Posts</h1> <table> <thead> <tr> <th>Title</th> <th>Author</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @posts.each do |post| %> <tr> <td><%= post.title %></td> <td><%= post.author %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table> <%= paginate @posts %> <br> <%= link_to 'New Post', new_post_path %>
Let's create some test data by running this script in a Rails console session:
100.times { |p| Post.create!(title: "Post #{p}", author: "Jon Snow") }
Start up the rails server and navigate to localhost:3000/posts
and you'll see our 100 posts and the pagination is working perfectly!
If you want to control how many records are shown per page you can pass in the per
method as shown below:
# app/controllers/posts_controller.rb def index @posts = Post.page(params[:page]).per(5) end
Refreshing the page will show that now only 5 records are being shown per page, I'm definitely a big fan of the syntax and clarity of the method calls.
The gem developer Akira Matsuda did a great job of showing all of the customization options on the Kaminari GitHub page, so I won't duplicate what he's already gone through there, I recommend you exploring each of the options.
Kaminari AJAX Implementation
I'm a huge fan of how easy it is to incorporate AJAX based pagination (pagination that doesn't require a page refresh). In fact, if I know that AJAX pagination is a requirement for an application I will use Kaminari
over will_paginate
since I'm not a huge fan of what it takes to incorporate AJAX into the will_paginate
calls.
The will_paginate
AJAX implementation always makes me feel a little dirty since it breaks some standard Rails conventions (which is why I didn't even go into it in the gem walkthrough). In contrast Kaminari's AJAX implementation follows standard Rails best practices. Let's refactor the application to include AJAX paging. First create a new javascript
file and add in the following code (JS script supplied by Akira Matsuda):
(function($) { // Make sure that every Ajax request sends the CSRF token function CSRFProtection(xhr) { var token = $('meta[name="csrf-token"]').attr('content'); if (token) xhr.setRequestHeader('X-CSRF-Token', token); } if ('ajaxPrefilter' in $) $.ajaxPrefilter(function(options, originalOptions, xhr){ CSRFProtection(xhr) }); else $(document).ajaxSend(function(e, xhr){ CSRFProtection(xhr) }); // Triggers an event on an element and returns the event result function fire(obj, name, data) { var event = new $.Event(name); obj.trigger(event, data); return event.result !== false; } // Submits "remote" forms and links with ajax function handleRemote(element) { var method, url, data, dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType); if (element.is('form')) { method = element.attr('method'); url = element.attr('action'); data = element.serializeArray(); // memoized value from clicked submit button var button = element.data('ujs:submit-button'); if (button) { data.push(button); element.data('ujs:submit-button', null); } } else { method = element.attr('data-method'); url = element.attr('href'); data = null; } $.ajax({ url: url, type: method || 'GET', data: data, dataType: dataType, // stopping the "ajax:beforeSend" event will cancel the ajax request beforeSend: function(xhr, settings) { if (settings.dataType === undefined) { xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); } return fire(element, 'ajax:beforeSend', [xhr, settings]); }, success: function(data, status, xhr) { element.trigger('ajax:success', [data, status, xhr]); }, complete: function(xhr, status) { element.trigger('ajax:complete', [xhr, status]); }, error: function(xhr, status, error) { element.trigger('ajax:error', [xhr, status, error]); } }); } // Handles "data-method" on links such as: // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a> function handleMethod(link) { var href = link.attr('href'), method = link.attr('data-method'), csrf_token = $('meta[name=csrf-token]').attr('content'), csrf_param = $('meta[name=csrf-param]').attr('content'), form = $('<form method="post" action="' + href + '"></form>'), metadata_input = '<input name="_method" value="' + method + '" type="hidden" />'; if (csrf_param !== undefined && csrf_token !== undefined) { metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />'; } form.hide().append(metadata_input).appendTo('body'); form.submit(); } function disableFormElements(form) { form.find('input[data-disable-with]').each(function() { var input = $(this); input.data('ujs:enable-with', input.val()) .val(input.attr('data-disable-with')) .attr('disabled', 'disabled'); }); } function enableFormElements(form) { form.find('input[data-disable-with]').each(function() { var input = $(this); input.val(input.data('ujs:enable-with')).removeAttr('disabled'); }); } function allowAction(element) { var message = element.attr('data-confirm'); return !message || (fire(element, 'confirm') && confirm(message)); } function requiredValuesMissing(form) { var missing = false; form.find('input[name][required]').each(function() { if (!$(this).val()) missing = true; }); return missing; } $('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) { var link = $(this); if (!allowAction(link)) return false; if (link.attr('data-remote') != undefined) { handleRemote(link); return false; } else if (link.attr('data-method')) { handleMethod(link); return false; } }); $('form').live('submit.rails', function(e) { var form = $(this), remote = form.attr('data-remote') != undefined; if (!allowAction(form)) return false; // skip other logic when required values are missing if (requiredValuesMissing(form)) return !remote; if (remote) { handleRemote(form); return false; } else { // slight timeout so that the submit button gets properly serialized setTimeout(function(){ disableFormElements(form) }, 13); } }); $('form input[type=submit], form button[type=submit], form button:not([type])').live('click.rails', function() { var button = $(this); if (!allowAction(button)) return false; // register the pressed submit button var name = button.attr('name'), data = name ? {name:name, value:button.val()} : null; button.closest('form').data('ujs:submit-button', data); }); $('form').live('ajax:beforeSend.rails', function(event) { if (this == event.target) disableFormElements($(this)); }); $('form').live('ajax:complete.rails', function(event) { if (this == event.target) enableFormElements($(this)); }); })( jQuery );
Don't worry about what this code is doing, just know that it is doing the majority of the work for implementing the AJAX implementation, including tasks such as escaping the params and processing the navigation without having to refresh the page.
Now create a new partial for our posts
and move the <tr>
tags into it:
<!-- app/views/posts/_post.html.erb --> <tr> <td><%= post.title %></td> <td><%= post.author %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr>
We need to now make several updates to the index
template:
Add an
id
tag to thetbody
tag that hold ourpost
iterator, this is because we will need to select this value in ajavascript
file we'll be creating shortly. We also need to add anid
tag to the pagination element.Let the
paginate
method know it needs to be able to respond tojs
calls by adding aremote: true
argumentCall the
posts
partial
Try to incorporate each of these changes before looking at the implementation below and then compare your implementation with mine:
<!-- app/views/posts/index.html.erb --> <p id="notice"><%= notice %></p> <h1>Listing Posts</h1> <table> <thead> <tr> <th>Title</th> <th>Author</th> <th colspan="3"></th> </tr> </thead> <tbody id="posts"> <%= render @posts %> </tbody> </table> <div id="paginator"> <%= paginate @posts, remote: true %> </div> <br> <%= link_to 'New Post', new_post_path %>
We're almost there! Now create an index.js.erb
file that we'll add our js
selectors in:
// app/views/posts/index.js.erb $('#posts').html('<%= escape_javascript render(@posts) %>'); $('#paginator').html('<%= escape_javascript(paginate(@posts, remote: true).to_s) %>');
There are a few things going on here:
We're selecting the
#post
ID via jQuery and rendering the@posts
collectionNext we're selecting the
#paginator
ID and running thepaginate
method on it with theremote
action set to true. Without theremote: true
call Rails wouldn't know that we want to render the call via JS
Startup the Rails server and refresh the /posts
index page and you'll see that our pagination is working with AJAX now! No more page refreshes and the results are changing just like before, very nice work!
Lastly, Kaminari can automatically implement styles from popular CSS frameworks. This sample app doesn't have any frameworks integrated, however if it did we could run the command:
rails generate kaminari:views bootstrap3
This will automatically create and style the pagination elements throughout the application. The bootstrap3
value is an argument that means that I want the styles associated with the Bootstrap framework, version 3. Kaminari has a great set of style options and you can implement the following themes:
bootstrap2
bootstrap3
bourbon
foundation
github
google
materialize
purecss
semantic_ui
In fact, the DevCamp platform uses Kaminari for pagination with the bootstrap3
theme.