Skip to content

Add Paginated Collection to a View (deprecated approach)

Patrick Bolger edited this page Jul 20, 2017 · 1 revision

The PETS pagination system is a modular and reusable library to adding AJAX-driven pagination to any view of a collection. It uses the will_paginate gem, and it provides an improved user experience by using javascript to change the page div on the view as the user navigates from page to page (without the javascript mechanism, each pagination update would cause a complete page reload).

This description of how to incorporate this functionality uses the example of "all company jobs" view on the company person home page. The primary files involved in this are:

  1. app/assets/javascripts/pagination.js - you should be aware that this is where the "heavy lifting" occurs, but should not have to analyze or understand the code in detail.
  2. app/views/company_people/home.html.haml - the view for the 'home' action of company_people_controller.rb
  3. jobs_controller.rb - this contains the action that will respond to an XHR action and return a rendered view (div) of the requested page of the paginated collection
  4. views/jobs/_list_all.html.haml - this renders a table with the jobs information. It also sets up pagination for the jobs with the will_paginate method.
  5. app/controllers/concerns/jobs_viewer.rb - this contains some helper functions for the jobs controller - especially a method to return a collection of jobs ready to be rendered in _list_all above.

The code in these files deliver pagination capability as described below.

app/views/company_people/home.html.haml

The code of interest here is:

%h3 All #{@company.name} jobs
.pagination-div{id: "jobs-#{@job_type}",
                data: {url: list_jobs_path(@job_type)}}

This uses the standard class 'pagination-div' to mark this specific div as containing content to paginate. The pagination JS code automatically finds and enables this div for pagination because of this standard class. (you could use a specific ID as well, but that technique involves additional javascript code (you must provide) and is not described in this document).

The ID here (jobs-<job type>) does not need to be of any specific format or content but, of course, it needs to be unique on the page. Note that this instance variable is set to its value in CompanyPeopleController#home.

The custom data attribute ('url') is the path to the action that will be called via XHR and return an updated div, containing the next page of the collection to display.

Note that this path must be the same path specified in the call to will_paginate (in the view).

app/controllers/concerns/jobs_viewer.rb

This defines a "concern" for jobs display. The code shown here is slightly different from the code in production at the time of this writing:

module JobsViewer
  extend ActiveSupport::Concern

  def display_jobs job_type, per_page = 10
    case job_type
      when 'my-company-all'
        return Job.paginate(:page => params[:jobs_page], :per_page => per_page).find_by_company(pets_user.company)
    end
  end

  FIELDS_IN_JOB_TYPE = {
      'my-company-all': [:title, :updated_at, :poster, :num_applicants]
  }

  def job_fields job_type
    FIELDS_IN_JOB_TYPE[job_type.to_sym] || []
  end

  # make helper methods visible to views
  def self.included m
    return unless m < ActionController::Base
    # make helper methods visible to views
    m.helper_method :job_fields # name of method above which enumerates fields for display
  end
end

The method display_jobs is used by the jobs controller to gather a collection of jobs to appear on the view (the paginate method - part of the will_paginate gem - will determine which set of jobs will be included in the next page to be displayed.

Here, the class method find_by_company is used to gather the records from the DB. In your implementation, you could use any class method that returns all or a subset of records from the DB in a consistent manner.

The method job_fields returns a list of symbols that represent the columns to be shown in the view for a particular type of jobs display. Here, there is only one type - 'my-company-all' - but other types could be added in order to show different views of the jobs data and/or show different subsets of the jobs data.

jobs_controller.rb

At the top of this file, the 'concern' module above is included:

include JobsViewer

The controller includes the 'list' action as:

def list
    raise 'Unsupported request' if not request.xhr?

    @job_type = params[:job_type] || 'my-company-all'

    @jobs = []
    @jobs = display_jobs @job_type
    render partial: 'list_all', :locals => {all_jobs: @jobs, job_type: @job_type}
end

This renders the partial that represents the next page of the collection (it is a div, with the ID as specified earlier, that will be used to replace the existing content of the current will_paginate page).

The var @job_type is specified as a parameter to the will_paginate method, which is described below. Note that this var must have a default value, since the first time the 'list' action will be executed that parameter will not be in the params hash for the action (it will be defined for all subsequent calls to the action).

Here, the @job_type value should correspond to a job_type value in the concern file (jobs_viewer.rb).

The @jobs var is assigned to the return value of display_jobs (defined in the jobs_viewer.rb).

views/jobs/_list_all.html.haml

As stated above, this renders a table showing the jobs made available for the specific page. Important aspects of the code are described below.

The table header values are created by:

%thead
    %tr
      - job_fields(job_type).each do |field|
        - case field
          - when :title
            %th.strong Title
          - when :updated_at
            %th.strong Last update
          - when :poster
            %th.strong Poster
          - when :num_applicants
            %th.strong Number of applications

Note that this uses the field names as defined in jobs_viewer.rb.

The table data rows are created by:

%tbody
    - all_jobs.each do |job|
      %tr
      - job_fields(job_type).each do |field|
        - case field
          - when :title
            %td.col-md-4
              = link_to(job.title, job_path(job))
          - when :updated_at
            %td.col-md-2
              = job.updated_at.strftime('%F')
          - when :poster
            %td.col-md-3
              = link_to(job.company_person.full_name, company_person_path(job.company_person))
          - when :num_applicants
            %td.col-md-2
              = job.number_applicants

Note that this also uses the field names as defined in jobs_viewer.rb.

At the end of the file, will_paginate is called:

= will_paginate all_jobs, param_name: 'jobs_page', 
                            :params => {:controller => 'jobs', 
                            :action => 'list', 
                            :job_type => job_type}

The assignment of 'param_name' gives a non-default name to the parameter used to determine which page to render next. It is assigned to a value that will differentiate this pagination from other pagination sections on the same page.

:param_name must be unique across all other pagination sections in the view. Note that it is assigned the same value as used in the display_jobs method in jobs_viewer.rb (for this particular job_type). (this is in the line:

return Job.paginate(:page => params[:jobs_page], :per_page => per_page).find_by_company(pets_user.company)

)

The params hash included as an argument to will_paginate defines the components of the URL to be used to render requested pages of the collection. This is the same URL as defined in the div statement in company_people/home.html.haml, with the addition of the :job_type parameter. This parameter is used by the controller action (jobs#list) to determine what type of jobs display to render (in this example there is only one - but there could be situations where different subsets of the same DB records are displayed in the same view, such as "Job Seekers who do not have a job developer" and "Job Seekers who do not have a case manager").