ruby-on-railsrubysearchpaginationpagy

Paginating Filtered Tables in Turbo Frame using Pagy?


I'm trying to implement pagination in my Rails project using the Pagy gem. I have a Record Model and index view for displaying my Records in a element and a form above the table to filter the table which is in a partial that contains the in a Turbo Frame.

My issue is that Pagy initially does paginate the search results (defined by 'records#search') inside the turbo frame but then when I click from one page to another page, it immediately goes to that page as if I am browsing unfiltered results (Record.all).

So a specific scenario is if I'm on the '/records/' route, but I want to filter for Records where name == 'foo'. As soon as I filter the table, it will show me the results on the table where name == 'foo' on page 1. Then if I click to go to page 2 to see more filtered results, it will be as if I'm on the unfiltered page 2 and I will lose my results where name == 'foo'.

So in my RecordsController I have:

class RecordsController < ApplicationController
  def index
    @pagy, @records = pagy(Record.all, items: 25)
  end

  def search
    records = Record.where('name LIKE ?', "%#{params[:name]}%")
    records = records&.where('type LIKE ?', "%#{params[:type]}%") if params[:type].present?

    render(partial: 'records_table', locals: { records: })
  end
end

In my index.html.erb for Records:

<h1>Records</h1>

<%= render "record_search_form" %>
<%= render "records_table",
    records: @records %>

<div class="pag-bar mt-5 pt-5">
  <%== pagy_bootstrap_nav(@pagy) if @pagy.pages > 1 %>
</div>

Inside the record_search_form partial:

<div style="margin:0.75rem">
    <%= form_with url: records_search_path, method: :post,
        data: {
                controller:"records-search",
                records_search_target: "form",
                turbo_frame:"records_table"} do |form| %>

        <%= form.text_field :name %>
        <%= form.text_field :type %>

        <%= form.button "Search", class:"btn btn-primary" %>
    <% end %>
</div>

Inside the records_table:

<%= turbo_frame_tag "records_table" do %>
    <table>
            <thead>
            <tr>
                <th>
                    ID
                </th>
                <th>
                    Name
                </th>
                <th>
                    Type
                </th>
            </tr>
            </thead>

            <tbody>
            <% if records.present? %>
                <% records.each do |record| %>
                    <tr>
                        <td><%= record.id %></td>
                        <td><%= record.name %></td>
                        <td><%= record.type %></td>
                    </tr>
                <% end %>
            <% end %>
            </tbody>
        </table>
<% end %>

I've tried altering my records#search action to render a different Pagy instance:

def search
    records = Record.where('name LIKE ?', "%#{params[:name]}%")
    records = records&.where('type LIKE ?', "%#{params[:type]}%") if params[:type].present?

    @pagy_results, @results = pagy(records, items: 25)

    render(partial: 'records_table', locals: { records: @results })
  end

And then including it in the view like this:

<h1>Records</h1>

<%= render "record_search_form" %>
<%= render "records_table",
    records: @records %>

<div class="pag-bar mt-5 pt-5">
  <%== pagy_bootstrap_nav(@pagy) if @pagy.pages > 1 %>
</div>

**<div class="pag-bar mt-5 pt-5">
  <%== pagy_bootstrap_nav(@pagy_search) if @pagy_search.present? && @pagy_search.pages > 1 %>
</div>**

This results in the issue occurring as before (when I click to go to another page of results, it will be as if I have gone to that page number but for unfiltered results).

I've also tried this inside records#search:

@pagy_search, @response = pagy(records, items: 100, pagy_url_for: 'records/search')

And doing this does not result in any changes either.


Solution

  • # app/controllers/records_controller.rb
    
    class RecordsController < ApplicationController
      def index
        scope = Record.all
        scope = scope.where("name LIKE ?", "%#{params[:name]}%") if params[:name].present?
        scope = scope.where("type LIKE ?", "%#{params[:type]}%") if params[:type].present?
    
        @pagy, @records = pagy(scope, items: 25)
      end
    end
    

    Pagination links should be inside the turbo frame, because you want them to be updated when navigating.

    target: "_top" - because you want other links to navigate outside of a frame and pagination links to be within a frame.
    autoscroll: true - scrolls to the top of search results.
    turbo_action: "advance" - updates current url.

    # app/views/records/index.html.erb
    
    <%= render "search_form" %>
    
    <%= turbo_frame_tag "records_table", target: "_top", autoscroll: true,
      data: {turbo_action: "advance"} do %>
    
      <%= render "table", records: @records %>
    
      <%== pagy_nav(@pagy, anchor_string: 'data-turbo-frame="_self"') %>
    <% end %>
    

    Almost every time, search should be a GET form:

    # app/views/records/_search_form.html.erb
    
    <%= form_with url: records_path, method: :get,
      data: {turbo_frame:"records_table"} do |form| %>
    
      <%= form.text_field :name, value: params[:name] %>
      <%= form.text_field :type, value: params[:type] %>
    
      <%= form.button "Search", name: nil %>
    <% end %>
    
    <!-- app/views/records/_table.html.erb -->
    
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Type</th>
        </tr>
      </thead>
      <tbody>
        <% records.each do |record| %>
          <tr>
            <td><%= record.id %></td>
            <td><%= record.name %></td>
            <td><%= record.type %></td>
          </tr>
        <% end %>
      </tbody>
    </table>