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.
Did you like this content?
Related Posts:
Slides from Atlrug
Slides from my presentation on AngularJS for the Atlanta Ruby Users Group
Speaking at Atlrug
I'm going to give a talk at AtlRug about our experience with AngularJS
The (Almost) Universality of the $onInit Lifecycle Callback in AngularJS
AngularJS 1.x's Component functionality brought with it new lifecycle callbacks that are not restricted to controllers within Components.