Pagination in AngularJS with Paginate-Anything and Kaminari

It takes a little bit of rails controller glue to connect angular-paginate-anything to Kaminari

Posted by Tejus Parikh on November 8, 2014

A code blogger once tweeted that learning a new framework is really just learning a new way of handling pagination. There is a very good pagination module for Angular JS and a very good pagination gem for Rails,but the two do not speak the same language. Instead of rolling my own solution that would not work as nicely, I decided to write a little bit of glue code to make the solutions compatible.

Within the web client, I required a pagination gem that would accept url parameters for filtering, handle the layout, be Bootstrap compatible, and have enough configuration options to handle multiple use-cases. Angular-paginate-anything clearly fit that bill. On the Rails side, our use of ActiveAdmin already brought in Kaminari. Joe Nelson, the author of the Angular module, does have a compatible Ruby gem, but I did not want to litter my code with two different pagination styles that depended on the caller.

The first step was to use angular-paginate-anything as intended:

<div class='paginated_container' ng-controller='PaginatedUserListCtrl'>
    <bgf-pagination url='users_url' url-params='user_params' collection='users' per-page="10"></bgf-pagination>
    <table>
    <tbody>
        <tr ng-repeat="user in users">
            <td></td>
        </tr>
    </tbody>
    </table>
</div>
angular.module('vijedi').
    .controller('PaginatedUserListCtrl', ['$scope', function($scope) {
        $scope.users_url = '/api/users/.json'; # defined here in case the url needs to be manipulated
        $scope.user_params = {
            status: 'active'
        };
    }])

That was straight forward. The problem is that angular-paginate-anything wants to paginate on ranges, but kaminari wants to paginate on pages. The range coming from the browser code needs to be translated into a page number and vis versa for the response. I choose to do this with custom before and after filters defined in the base controller of my API. My approach was inspired by the clean_pagination gem and Javier Saldana's pagination with ActiveResource blog post.

class Api::BaseController < ApplicationController
    include Rivalry::OrganizationScope
    respond_to :json

    private 
    def self.paginated_action(options = {})
        before_filter(options) do |controller|
            if request.headers['Range-Unit'] == 'items' &&
                    request.headers['Range'].present?

                requested_from = nil
                requested_to = nil

                if request.headers['Range'] =~ /(\d+)-(\d*)/
                    requested_from, requested_to = $1.to_i, ($2.present? ? $2.to_i : Float::INFINITY)
                end

                if (requested_from > requested_to)
                  response.status = 416
                  headers['Content-Range'] = "*/#{total_items}"
                  render text: 'invalid pagination range'
                  return false
                end

                @kp_per_page = requested_to - requested_from + 1
                @kp_page = requested_to / @kp_per_page + 1
            end
        end

        after_filter(options) do |controller|
            results = instance_variable_get("@#{controller_name}") # ex @users
            if(results.length > 0)
                response.status = 206
                headers['Accept-Ranges'] = 'items'
                headers['Range-Unit'] = 'items'

                total_items = results.total_count
                current_page = results.current_page
                per = @kp_per_page

                requested_from = (results.current_page - 1) * per
                available_to = (results.length - 1) + requested_from

                headers['Content-Range'] = "#{requested_from}-#{available_to}/#{total_items < Float::INFINITY ? total_items : '*'}"
            else 
                response.status = 204
                headers['Content-Range'] = "*/0"
            end
        end
    end
end

My method makes kp_page and kp_per_page (kp for KaminariPagination) available as instance variables. Values could also be put into the params hash.

Finally, the ActionController needs to set the filter and use the newly scoped instance variables for paging.

class Api::UsersController < Api::BaseController
    paginated_action only: [:index]

    def index
        @users = User.by_name.where(status: params[:status]) # needs to be assigned so the lookup in the filter works 
            .page(@kp_page)
            .per(@kp_per_page)

        respond_with @users
    end
end

Now I've got a paginated setup that works well and looks idiomatic in both Angular and Rails. I've also made this code available as a gist for easier sharing and modifications.

Related Posts:

Tejus Parikh

Tejus is an software developer, now working at large companies. Find out when I write new posts on twitter, via RSS or subscribe to the newsletter: