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.
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
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
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
Prefer to use view components for some of the core items required.
- Create pagy paginator component (https://gist.github.com/john-hamnavoe/af5ee9e09033da174f995e585bf0522a)
- Create table components (https://gist.github.com/john-hamnavoe/9cb45c585bccf9f3700afc6a1e6631de)
- Create index container component (https://gist.github.com/john-hamnavoe/520d894edb71c38fc94df12148e03698)
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.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
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 %>