-
Notifications
You must be signed in to change notification settings - Fork 37
Add JS pagination to a view
This article is about adding pagination to a table in a view. By that, we mean:
- Show portions of a collection of objects in separate pages,
- Provide sort-by-column (ascending and descending), and,
- Allow the user to select the number of items to appear on a page.
The pagination uses the will_paginate
and ransack
gems, but also includes some JavaScript scaffolding to enable an update to a paginated table by updating the DOM element that contains the table as opposed to a complete page load. This prevents the page from being repositioned at the top of the page when a pagination link is clicked by the user.
This results in a much better user experience than just using "vanilla" will_paginate
functionality.
The pagination that is used in the companies index view is used as an example.
Reference material for this includes:
The example code used here can be seen in its entirety at app/views/companies/_companies_list.html.haml
Add standard pagination helpers to the table (if you are using ransack for DB search and/or you want to allow the user to sort by column):
ransack
has built-in support for sorting by column via the sort_link
helper. For example:
#companies_list
%table.table.table-hover
%thead
%th
= t('activerecord.models.business_category.one')
%th
= sort_link(@search_params, :name,
t('activerecord.attributes.company.name'), {},
{ class: 'companies_pagination', remote: true })
%th
= sort_link(@search_params, :addresses_region_name,
t('activerecord.attributes.address.region'), {},
{ class: 'companies_pagination', remote: true })
%th
= sort_link(@search_params, :addresses_kommun_id,
t('activerecord.attributes.address.kommun'), {},
{ class: 'companies_pagination', remote: true })
Note that the last hash argument to sort_link is explained here.
In the code snippet above the table is contained in a div with ID == #companies_list
.
We will use that ID later in order to replace the div with a new pagination page.
= render partial: 'application/paginate_footer',
locals: { entities: @companies,
paginate_class: 'companies_pagination',
items_count: @items_count,
url: companies_path }
The footer looks like this:
- paginate_links = will_paginate entities,
renderer: RemoteLinkPaginationHelper::BootstrapLinkRenderer,
class: paginate_class,
params: defined?(params) ? params : nil
-#
Links will not be shown if too few items, or if items-per-page is set to "All".
For the latter, still need to show items-per-page selection.
- if paginate_links || entities.count > PaginationUtility::DEFAULT_ITEMS_SELECTION
.row.center-aligned-container
.col-sm-8.col-sm-offset-2.center
= paginate_links
.col-sm-2.center
-# override min-width from 'custom.css' - too wide
= select_tag(:items_count, paginate_count_options(items_count),
data: { remote: true,
url: url },
style: 'min-width: 50px;',
class: paginate_class )
%span.glyphicon.glyphicon-info-sign{ title: "#{t('items_per_page_tooltip')}",
data: {toggle: 'tooltip'} }
In the code above, we are:
- Using a renderer (WillPaginate::ActionView::BootstrapLinkRenderer)provided by the
will_paginate-bootstrap
gem (seemodule RemoteLinkPaginationHelper
to see how that is used), - Specifying
link_options
that will result is an XHR request being sent to the controller (read about theremote: true
option for Rails links in the reference cited above), and, - Specifying a class (here,
.companies_pagination
) that will be applied to all of the pagination links (we'll use that later). This class should be unique to this particular paginated table on this page (that is, if another paginated table is present on this page also then that table should have another class specified here), and, - Adding a
select
tag that allows the user to select how many items to show on a pagination page.
For more information, see those links:
https://gist.github.com/jeroenr/3142686, and
https://github.com/bootstrap-ruby/will_paginate-bootstrap
This includes responding to pagination link invocations as well as selecting number of items to show on the page. The related controller logic looks like this:
action_params, @items_count, items_per_page = process_pagination_params('company')
@search_params = Company.ransack(action_params)
@all_companies = @search_params.result(distinct: true)
.complete
.includes(:business_categories)
.includes(addresses: [ :region, :kommun ])
.joins(addresses: [ :region, :kommun ])
@all_visible_companies = @all_companies.address_visible
@all_visible_companies.each { | co | geocode_if_needed co }
@companies = @all_companies.page(params[:page]).per_page(items_per_page)
respond_to :js, :html
include PaginationUtility
The first line above uses a shared helper method (actually, a controller concern) to process the action params
hash and set the 3 variables shown. The action_params
var is used for searching (via the Ransack gem). The @items_count
is the selected number of items per page (this could be an integer are "All"), and the actual number of items per page (which is always an integer) to be used setting up the pagination call.
The last line in the code whitelists the types of requests the controller will respond to. In this case, the controller will render a JS file that, in turn, will update the DOM element containing the paginated table.
(see below for another way to handle updating the paginated table).
Add a view file - views/companies/index.js.erb
that will render the pagination element. The contents:
$('#companies_list').html("<%= j(render 'companies_list', companies: @companies) %>");
// In case there is tooltip(s) in rendered element:
$('[data-toggle="tooltip"]').tooltip();
This javascript will render the companies_list
partial and replace the pagination element in the DOM with that rendered HTML. We wrap this in ERB so that we can use the instance variable @companies
set up by the controller.
Instead of rendering a JS template, the controller could render an HTML template. Then, since we are operating within the context of an XHR request, we can update the DOM by using a JS callback that triggers upon a successful AJAX request (which is triggered when the controller renders the HTM template).
For example, this code is from users_controller#index:
render partial: 'users_list',
locals: { q: @q, users: @users, items_count: @items_count } if request.xhr?
This renders just the paginated table that lists all users. (if the request is not an XHR request, then the controller executes the default action of rendering the index HTML template).
Then, this code in assets/javascripts/users.js
comes into play:
$('body').on('ajax:success', '.users_pagination', function (e, data) {
$('#users_list').html(data);
$('[data-toggle="tooltip"]').tooltip();
});
The second line finds the table (via ID) and replaces that element with the HTML created by the controller rendering action (passed in as argument data
to the callback function.