Skip to content

Add JS pagination to a view (preferred approach)

Arun Kumar edited this page Oct 3, 2017 · 5 revisions

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:

  1. will_paginate gem
  2. ransack gem
  3. Rails Guide - Working with Javascript in Rails

Step 1: Modify the table view

The example code used here can be seen at app/views/jobs/_search_job_list.html.haml

Add sort pagination helpers to the table header elements

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.

Embed the table in a div with an ID

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 the "shared/pagination_footer" partial below the table:

= render partial: 'shared/paginate_footer',
           locals: { entities: @jobs,
                     paginate_class: 'searched_jobs_pagination',
                     items_count: @items_count,
                     url: jobs_path }
Aside: Pagination Footer Explained

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:

  1. Using a renderer provided by the will_paginate-bootstrap gem,
  2. Specifying link_options that will result is an XHR request being sent to the controller (read about the remote: true option for Rails links in the reference cited above) and,
  3. 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,
  4. 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

Step 2: Enable controller action to manage pagination requests

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 this helper (concern) module at the top of the controller file:

include PaginationUtility

Call process_pagination_params

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.

Enable controller action to respond to XHR request

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.

Step 3: Add JS to handle the pagination refresh

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.

Clone this wiki locally