ruby-on-rails - 使用 Hotwire 无限滚动分页和过滤

标签 ruby-on-rails hotwire-rails turbo turbo-frames hotwire

我有一个无限滚动的表格,可以完美地工作,无需重新加载整个页面。我现在在添加过滤器时遇到问题。感谢 Phil Reynolds 的文章 https://purpleriver.dev/posts/2022/hotwire-handbook-part-2我能够实现无限负载。

Controller Action

def index
  if params[:query].present?
    search = "%#{params[:query]}%"
    alerts = Alert.where( "title ILIKE ?", search )
  else
    alerts = Alert.all
  end

  @pagy, @alerts = pagy(alerts, items: 100)
end

表格

<%= turbo_frame_tag "page_handler" %>
<table class="w-full border border-t-gray-300 table-auto">
  <thead>
    <tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-3">
      <th class="py-3 px-4 text-left">Severity</th>
      <th class="py-3 px-3 text-left">Title</th>
      ...
    </tr>
  </thead>
    <tbody id="alerts" class="text-gray-600 text-sm font-light">
      <%= render "alerts_table", alerts: @alerts %>
    </tbody>
</table>
<%= render "shared/index_pager", pagy: @pagy %>

alerts_pager 部分

<div id="<%= controller_name %>_pager" class="min-w-full my-8 flex justify-center">
  <% if pagy.next %>
    <%= link_to 'Loading',
                "#{controller_name}?query=#{params[:query]}&page=#{pagy.next}",
                data: {
                  turbo_frame: 'page_handler',
                  controller: 'autoclick'
                },
                class: 'rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800'%>
  <% end %>
</div>

涡轮帧响应

<%= turbo_frame_tag "page_handler" do %>
    <%= turbo_stream_action_tag(
        "append",
        target: "alerts",
        template: %(#{render "alerts_table", alerts: @alerts}) 
    ) %>
    <%= turbo_stream_action_tag(
        "replace",
        target: "alerts_pager",
        template: %(#{render "shared/index_pager", pagy: @pagy})
        ) %>
<% end %>

自动点击 Controller

import { Controller } from "@hotwired/stimulus"
import { useIntersection } from 'stimulus-use'

export default class extends Controller {
  options = {
    threshold: 0.5
  }

  connect() {
    useIntersection(this, this.options)
  }

  appear(entry) {
    this.element.click()
  }
}

我还设法使其与过滤器一起工作,但它会重新加载整个页面。

<div id="<%= controller_name %>_filter" class="bg-gray-200 p-1 shadow-lg">
    <div class="p-1 lg:w-1/3">
        <%= form_with url: alerts_path,  method: :get do %>
            <%= text_field_tag "query", 
                                                    nil, 
                                                    placeholder: "Filter", 
                                                    class: "inline-block rounded-md border border-gray-200 outline-none px-3 py-2 w-full" %>
        <% end %>
    </div>
</div>

我想更新同一 Turbo 帧中的内容。但问题是page_handler帧中的turbo_stream_action_tag追加了数据。是否需要另一个 Turbo_frame_tag 来服务过滤器?如何实现?

我尝试添加<%= turbo_frame_tag "filter_handler" %>到索引页并添加以下部分以进行涡轮帧响应

<%= turbo_frame_tag "filter_handler" do %>
    <%= turbo_stream_action_tag(
        "replace",
        target: "alerts",
        template: %(#{render "alerts_table", alerts: @alerts}) 
    ) %>
<% end %>

并添加data: {turbo_frame: "filter_handler"}过滤器的属性。但它工作不正确

最佳答案

您可以为表单添加turbo_stream响应,并执行更新替换操作,而不是附加。为了测试它,我制作了无限滚动的更简单版本,但它应该工作相同:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  include Pagy::Backend

  # GET /posts
  def index
    scope = Post.order(id: :desc)
    scope = scope.where(Post.arel_table[:title].matches("%#{params[:query]}%")) if params[:query]
    @pagy, @posts = pagy(scope)

    respond_to do |format|
      # this will be the response to the search form request
      format.turbo_stream do
        render turbo_stream: turbo_stream.replace(:infinite_scroll, partial: "infinite_scroll")
      end
      # this is regular navigation response
      format.html
    end
  end
end
# app/views/posts/index.html.erb

# NOTE: set up a GET form and make it submit as turbo_stream
#                                     vvv          vvvvvvvvvvvvvvvvvv
<%= form_with url: "/posts", method: :get, data: { turbo_stream: true } do |f| %>
  <%= f.search_field :query %>
<% end %>
<%= render "infinite_scroll" %>
# app/views/posts/_infinite_scroll.html.erb

<div id="infinite_scroll">
  <%= turbo_frame_tag "page_#{params[:page] || 1}", target: :_top do %>
    <hr><%= tag.h3 "Page # #{params[:page] || 1}", class: "text-2xl font-bold" %>

    <% @posts.each do |post| %>
      <%= tag.div post.title %>
    <% end %>

    <% if @pagy.next %>
      # NOTE: technically there is no need for `turbo_stream.append` here
      #       but without it turbo frames will be nested inside each other
      #       which works just fine.
      #       also, i'm not sure why `turbo_stream_action_tag` is used.
      <%= turbo_stream.append :infinite_scroll do %>
        <%= turbo_frame_tag "page_#{@pagy.next}", target: :_top, loading: :lazy, src: "#{controller_name}?query=#{params[:query]}&page=#{@pagy.next}" %>
        # NOTE:                       this bit is also important ^^^^^^^^^^^^^^
      <% end %>
    <% end %>
  <% end %>
</div>

您也可以将整个内容包装在另一个框架中:

<%= form_with url: "/posts", method: :get, data: { turbo_frame: :infinite_frame, turbo_action: :advance } do |f| %>
  <%= f.search_field :query %>
<% end %>
<%= turbo_frame_tag :infinite_frame do %>
  <%= render "infinite_scroll" %>
<% end %>

在这种情况下,不需要 format.turbo_stream index 操作中的响应。


如果有人想知道它是如何工作的,看到比解释更容易,所以这就是它最初呈现的内容:

<div id="infinite_scroll">
  <turbo-frame id="page_1" target="_top">
    <hr><h3 class="text-2xl font-bold">Page # 1</h3>
    <!-- page 1 posts -->
  </turbo-frame>

  <!-- NOTE: this frame is not loaded yet -->
  <turbo-frame loading="lazy" id="page_2" src="posts?query=&amp;page=2" target="_top"></turbo-frame>
</div>

向下滚动到 page_2 后框架,它发送下一页请求,其中将有 page_2框架尚未加载page_3框架:

<div id="infinite_scroll">
  <turbo-frame id="page_1" target="_top">
    <hr><h3 class="text-2xl font-bold">Page # 1</h3>
    <!-- page 1 posts -->
  </turbo-frame>

  <!-- NOTE: page 2 frame is loaded and updated -->
  <turbo-frame loading="lazy" id="page_2" src="http://localhost:3000/posts?query=&amp;page=2" target="_top" complete="">
    <hr><h3 class="text-2xl font-bold">Page # 2</h3>
    <!-- page 2 posts -->
  </turbo-frame>

  <!-- NOTE: and just keep scrolling -->
  <turbo-frame loading="lazy" id="page_3" src="posts?query=&amp;page=3" target="_top"></turbo-frame>
</div>

无限滚动表格

它不适用于表格,因为你不能有 <turbo-frame>里面的标签<tbody>标签。只需滚动到表格之外并附加行,这就是您之前所做的。但这是一个工作示例,所有内容都适合单个模板,没有部分内容:

<!-- app/views/posts/index.html.erb -->

<!-- when searching, just replace the whole inifinite scroll part -->
<%= form_with url: "/posts", method: :get, data: { turbo_frame: :infinite_frame, turbo_action: :advance } do |f| %>
  <%= f.search_field :query, value: params[:query] %>
<% end %>

<!-- you can put this into a partial instead -->
<% rows = capture do %>
  <tr colspan="2">
    <th class="px-3 py-3 text-left">Page <%= params[:page]||1 %></th>
  </tr>
  <% @posts.each do |post| %>
    <tr class="">
      <th class="px-3 py-3 text-left"><%= post.id %></th>
      <th class="px-3 py-3 text-left"><%= post.title %></th>
    </tr>
  <% end %>
<% end %>

<!--
  to avoid appending the first page and just render it, we need to 
  differentiate the first request from subsequent page_2, page_3
  turbo frame requests
-->
<% infinite_scroll_request = request.headers["Turbo-Frame"] =~ /page_/ %>

<!--
  the search will also work without this frame
  but this way it won't update the whole page
-->
<%= turbo_frame_tag :infinite_frame, target: :_top do %>
  <!--
    render the first page on initial request, we don't need the whole 
    table again on subsequent requests
  -->
  <% unless infinite_scroll_request  %>
    <table class="w-full border table-auto border-t-gray-300">
      <thead>
        <tr class="text-sm text-gray-600 uppercase bg-gray-200 leading-3">
          <th class="px-3 py-3 text-left">ID</th>
          <th class="px-3 py-3 text-left">Title</th>
        </tr>
      </thead>
      <tbody id="infinite_rows" class="text-sm font-light text-gray-600">
        <%= rows %>
      </tbody>
    </table>
  <% end %>

  <div id="infinite_scroll">
    <%= turbo_frame_tag "page_#{params[:page] || 1}", target: :_top do %>
      <!-- render the next page and append it to tbody -->
      <% if infinite_scroll_request %>
        <%= turbo_stream.append :infinite_rows do %>
          <%= rows %>
        <% end %>
      <% end %>
      <% if @pagy.next %>
        <%= turbo_stream.append :infinite_scroll do %>
          <%= turbo_frame_tag "page_#{@pagy.next}", target: :_top, loading: :lazy, src: "#{controller_name}?query=#{params[:query]}&page=#{@pagy.next}" do %>
            <b>loading...</b>
          <% end %>
        <% end %>
      <% end %>
    <% end %>
  </div>
<% end %>

关于ruby-on-rails - 使用 Hotwire 无限滚动分页和过滤,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75266559/

相关文章:

stimulusjs - 具有具有相同 Controller 的嵌套元素

pascal - 如何解决Turbo pascal错误: line too long

ruby-on-rails - rails : Turbo Frame/Turbo Stream element is not rendered the SECOND time I trigger it

mysql - 如何在 Mac OS 10.6.6 上安装 gem MySQL

mysql - 无法使用 RubyMine 连接到正确的 mysql 安装

ruby-on-rails - 如何通过 Hotwire Turbo 流正确渲染图像?

ruby-on-rails - 如何使用 Turbo/Stimulus 插入新记录的表格?

ruby-on-rails - 保存拖放 rails 中的位置

ruby-on-rails - 我如何在 Rails 中一次保存多条记录?