Skip to content

Instantly share code, notes, and snippets.

@john-hamnavoe
Last active July 16, 2024 15:42
Show Gist options
  • Save john-hamnavoe/ec3ccf1bde282ada07121469b2430bd3 to your computer and use it in GitHub Desktop.
Save john-hamnavoe/ec3ccf1bde282ada07121469b2430bd3 to your computer and use it in GitHub Desktop.
How I set up pagy and ransack for simple rails 7 application

Introduction

Adding pagy and ransack in rails so can have search function, paging and sorting on table of date on index controller. We also will use kredis to make it easy to store users last search/sort etc. between pages.

Install

Gemfile:

gem "pagy" gem "ransack"

Uncomment out redis and kredis from the Gemfile also.

Run bundle install make sure all gems installed.

Run ./bin/rails kredis:install to add a default configuration at config/redis/shared.yml

Configure pagy

In application_controller.rb add include Pagy::Backend
In application_helper.rb and include Pagy::Frontend

Create pagy.rb file in the config/intializers folders you can start with this: https://github.com/ddnexus/pagy/blob/master/lib/config/pagy.rb

Change where it has require 'pagy/extras/overflow' to be the following:

require 'pagy/extras/overflow'
Pagy::DEFAULT[:overflow] = :last_page   # default  (other options: :last_page and :exception)

In app/helpers create pagy_helper.rb

module PagyHelper
  include Pagy::Frontend

  def pagy_links(**args, &block)
    tag.div(id: "pagy-links", **args, data: {controller: "toggle", toggle_visibility_class: "hidden", toggle_target: "element", "toggle-toggle-on-connect-value": true}, &block)
  end
end

Configure ransack

Create ransack.rb file in the config/intializers folders you can start with this:

Ransack.configure do |c|
  # Change default search parameter key name.
  # Default key name is :q
  c.search_key = :query
end

Create View Components

Prefer to use view components for some of the core items required.

  1. Create pagy paginator component (https://gist.github.com/john-hamnavoe/af5ee9e09033da174f995e585bf0522a)
  2. Create table components (https://gist.github.com/john-hamnavoe/9cb45c585bccf9f3700afc6a1e6631de)
  3. Create index container component (https://gist.github.com/john-hamnavoe/520d894edb71c38fc94df12148e03698)

User Model

Update your user.rb to have kredis hashes, add:

# Keep track of ransack search parameters to reset after edit for example
kredis_hash :index_settings
kredis_hash :index_searches

Application Controller

application_controller.rb has new method ransack_query that controller index actions will call

class ApplicationController < ActionController::Base
  include Pagy::Backend

  protected

  def ransack_query(model, default_sort = "", default_filters = {})
    page = current_page

    params[:query] = {} if params[:query].nil?

    apply_sort_to_ransack_params(default_sort)

    apply_query_to_ransack_params(default_filters)
    
    # this can be expanded to have default filters applied to all queries
    # e.g.  query = model.column_names.include?("organisation_id") ? model.where(organisation_id: current_user.current_organisation.id) : model
    query = model     

    ransack_query = query.ransack(params[:query])
    
    return ransack_query, page
  end

  private

  def current_page
    # update kredis hash with current page if user has changed page then return current page from kredis hash or default to 1
    page_key = search_hash_key("page")
    current_user.index_settings.update(page_key =>  params[:page]) if params[:page].present?
    current_user.index_settings[page_key] || 1
  end

  def apply_sort_to_ransack_params(default_sort)
    # update kredis hash with current sort if user has changed sort then return sort from kredis hash or default to default_sort
    sort_key = search_hash_key("sort")
    current_user.index_settings.update(sort_key =>  params[:query][:s]) if params[:query][:s].present?
    params[:query][:s] = current_user.index_settings[sort_key]|| default_sort
  end

  def apply_query_to_ransack_params(default_filters)
    # update kredis hash with current query if user has changed query then return query from kredis hash and apply any default filters
    params[:query].each do |key, value|
      next if key == "s"
      query_key = search_hash_key(key)
      current_user.index_searches.update(query_key =>  value)
    end

    current_user.index_searches.keys.each do |key|
      query_key = search_query_key_from_hash_key(key)
      next if query_key.nil?
      params[:query][query_key] = current_user.index_searches[key] if params[:query][query_key].nil?
    end

    default_filters.each do |key, value|
      params[:query][key] = value if params[:query][key].nil?
    end
  end

  def search_hash_key(name, path = nil)
    full_path = path&.split("?") || request.fullpath.split("?")
    session_path = full_path.first.split("/")
    "#{session_path.second_to_last}_#{session_path.last}_#{name}"
  end

  def search_query_key_from_hash_key(hash_key)
    base_key = search_hash_key("")
    return nil unless hash_key.start_with?(base_key)

    hash_key.split(base_key).last
  end
end

A typical index method in controller would look like this the model is Team which has some assocations Leader, Deputy and name and active columns. (Ignore the XLSX stuff in most cases this supports option to download into Excel)

  def index
    @query, page = ransack_query(Team.includes(:leader).includes(:deputy), "name asc", {active_eq: 1})

    @pagy, @teams = pagy(@query.result, page: page)
  end

index.html.erb

A index.html.erb using components above to display data in table, with search box, and sorting on columns.

<%= render(IndexContainerComponent.new(title: "Teams", new_path: new_team_path, sub_title: "Teams in your organistion, you manage")) do |c| %>
  <% c.search_form do %>
    <%= render "shared/ransack_search_form", frame: "teams", placeholder: "teams", query: @query, search_field_name: :name_cont, active_filter: true %>
  <% end %>
  <%= turbo_frame_tag "teams" do %>
    <%= render Tables::TableComponent.new do |table| %>
      <% table.with_header do |header| %>
        <% sort_link(@query, :name) %>
      <% end %>
      <% table.with_header small_visible: false do |header| %>
        <% sort_link(@query, :address) %>
      <% end %>
      <% table.with_header small_visible: false do |header| %>
        <% sort_link(@query, :tel_no) %>
      <% end %>      
      <% table.with_header do |header| %>
        <% sort_link(@query, :active) %>
      <% end %>    
      <% table.with_header align: :right do %>
        <span class="sr-only">Actions</span>
      <% end %>
      <% @teams.each do |team| %>
        <% table.with_row do |row| %>
          <% row.with_cell primary: true do %>
            <%= link_to team.name, edit_team_path(team), class: "block" %>
          <% end %>
          <% row.with_cell small_visible: false do %>
            <%= link_to team.address.to_s.truncate(80), edit_team_path(team), class: "block" %>        
          <% end %>
          <% row.with_cell small_visible: false do %>
            <%= link_to team.tel_no.to_s, edit_team_path(team), class: "block" %>        
          <% end %>          
          <% row.with_cell do %>
            <%= render Tables::CheckboxCellComponent.new(team.active) %>
          <% end %>        
          <% row.with_cell align: :right do %>
            <%= render LinkComponent.new("Edit", edit_team_path(team), data: { turbo_frame: :_top }) %>
          <% end %>
        <% end %>
      <% end %>
    <% end %>
    <%= render PagyPaginatorComponent.new(id: "teams", pagy: @pagy) %>
  <% end %>  
<% end %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment