Kaminari
Learn how to incorporate and customize the Kaminari pagination gem, including how to incorporate AJAX based pagination.
Guide Tasks
  • 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!

large

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 the tbody tag that hold our post iterator, this is because we will need to select this value in a javascript file we'll be creating shortly. We also need to add an id tag to the pagination element.

  • Let the paginate method know it needs to be able to respond to js calls by adding a remote: true argument

  • Call 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 collection

  • Next we're selecting the #paginator ID and running the paginate method on it with the remote action set to true. Without the remote: 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.

Code

Resources