Skip to content

Add JS pagination to a view

Patrick Bolger edited this page Oct 30, 2020 · 28 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 companies 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 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.

Embed the table in a div with an ID

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 the "application/pagination_footer" partial

= render partial: 'application/paginate_footer',
           locals: { entities: @companies,
                     paginate_class: 'companies_pagination',
                     items_count: @items_count,
                     url: companies_path }
Aside: Pagination Footer Explained

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:

  1. Using a renderer (WillPaginate::ActionView::BootstrapLinkRenderer)provided by the will_paginate-bootstrap gem (see module RemoteLinkPaginationHelper to see how that is used),
  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, .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,
  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:

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 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 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.

Enable controller action to respond to XHR request

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).

Step 3: Add JS to handle the pagination refresh

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.

Alternative mechanism for server to update the paginated table in the DOM

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.

Clone this wiki locally