-
Notifications
You must be signed in to change notification settings - Fork 66
Add JS pagination to a view (preferred approach)
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 jobs search (jobs index) view is used as an example.
Reference material for this includes:
The example code used here can be seen at app/views/jobs/_search_job_list.html.haml
ransack
has built-in support for sorting by column - make sure you leverage that, even if you don't need to use ransack
for it's DB search capability. For example:
#searched-job-list
%table.table.table-hover
%thead
%tr
%th
= sort_link(@query, :title, {}, { class: 'searched_jobs_pagination',
remote: true })
%th
= sort_link(@query, :description, {}, { class: 'searched_jobs_pagination',
remote: true })
%th
= sort_link(@query, :company_name, 'Company', {},
{ class: 'searched_jobs_pagination', remote: true })
%th
= sort_link(@query, :shift, {}, { class: 'searched_jobs_pagination',
remote: true })
%th
= sort_link(@query, :address_state, {}, { class: 'searched_jobs_pagination',
remote: true })
%th
= sort_link(@query, :address_city, {}, { class: 'searched_jobs_pagination',
remote: true })
%th
= sort_link(@query, :status, {}, { class: 'searched_jobs_pagination',
remote: true })
Note that the last hash argument to sort_link is explained here.
In the above code snippet, the table is contained in a div with ID == #searched-job-list.
We will use that ID later, in a javascript callback function, in order to replace the div with a new pagination page.
= render partial: 'shared/paginate_footer',
locals: { entities: @jobs,
paginate_class: 'searched_jobs_pagination',
items_count: @items_count,
url: jobs_path }
The footer looks like this:
.row.center-aligned-container
.col-sm-8.col-sm-offset-2{ style: 'text-align: center;' }
= will_paginate entities,
renderer: BootstrapPagination::Rails,
link_options: { 'data-remote': true,
class: paginate_class }
.col-sm-2{ style: 'text-align: right;' }
= select_tag(:items_count, paginate_count_options(items_count),
data: { remote: true,
url: url },
class: paginate_class )
%span.glyphicon.glyphicon-info-sign{ title: 'Select number of items per page',
data: {toggle: 'tooltip'} }
In the code above, we are:
- Using a renderer provided by the
will_paginate-bootstrap
gem, - 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,
searched_jobs_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 (jobs_controller#index):
search_params, @items_count, items_per_page = process_pagination_params('searched_jobs')
.... (some processing unique to this search feature) ....
@query = Job.ransack(search_params) # For form display of entered values
@jobs = Job.ransack(q_params).result
.includes(:company)
.includes(:address)
.page(params[:page]).per_page(items_per_page)
render partial: 'searched_job_list' if request.xhr?
Note that in the above code we are using a restructured version of the query parameters in the variable q_params
. Without the need for that special processing (generally not needed for other applications) we would just have used the variable search_params
in a single call to Job.ransack
.
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 search_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 or "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 above references the partial (searched_job_list
) that contains the paginated table. The updated content on the view with be rendered and returned as a response to an XHR request.
In pagination.js
, add a callback function (in the DOM-ready function at the bottom of the file) that looks like this (modified appropriately, of course):
$('body').on('ajax:success', '.searched_jobs_pagination', function (e, data) {
$('#searched-job-list').html(data);
});
Here, we are adding a callback function for all elements on the page that have class .searched_jobs_pagination
. As we saw above, that is the class we gave to the pagination links ("Next", "Previous", etc.).
The callback fires when an XHR request, initiated by the user clicking on one of the links, completes successfully. This happens when the controller renders the updated DOM elements from the controller action above.
The rendered HTML returned by the controller is represented here in the data
argument to the callback handler function. That function simply replaces the existing div containing the paginated table with the updated div.
Note that here we are delegating the callback handling to the body of the DOM. This is because the pagination links that are associated with the callback will be replaced when the div is replaced. Delegating to the body ensures that the callback will still be enabled for the replacement links.