🈲

RailsとStimulusjsで非同期に一覧のページネーションする

2024/04/11に公開

昔のコードでは、$('#paginator').on 'ajax:success',...みたいにcallbackをフックに書き換えたり、
https://qiita.com/shota6/items/843ff8f45bc4ece4395f

js.erbのレスポンスでHTMLを書き換えるような$('#pagenate').html("<%= escape_javascript(render 'index_page') %>"); をやっていました。
https://pistachio0416.hatenablog.com/entry/2015/03/12/Kaminariをjsonでajax化する

Stimulusjsを使った場合は、どう実装するか、ということを書きます。
ページネーションのライブラリにはkaminariを使います。

以下のテンプレートのページネーションをajaxで行うように修正していきます。
今回は、ソースコードを公開していないので、謎の断片が混ざるのは一般化することがめんどくさくなっているだけで、仕様です。

<h1>index ページです</h1>
<%= paginate @video_chats, remote: true %>

<% @video_chats.each do |video_chat| %>
  <div>
    <%= video_chat.text %>
  </div>
<% end %>

--

処理の全体の流れは、Stimulusjsのcontrollerから、非同期リクエストの呼び出しとHTMLの書き換えを行います。
Stimulusjsのcontrollerは、以下の通りです。

import { Controller } from "@hotwired/stimulus";

// Connects to data-controller="video-chats-loader"
export default class extends Controller {
  static values = { url: String, keyword: String };

  connect() {
    this.load();
  }

  load(page) {
    this.element.innerHTML = 'Loading...';
    let url = this.urlValue;

    if (page) {
      url += `${url.includes("?") ? "&" : "?"}page=${page}`;
    }

    if (this.keywordValue) {
      url += `${url.includes("?") ? "&" : "?"}keyword=${encodeURIComponent(
        this.keywordValue
      )}`;
    }
    fetch(url)
      .then((response) => response.text())
      .then((html) => (this.element.innerHTML = html));
  }
}

htmlの修正は、<% @video_chats.each do |video_chat| %>...の部分をパーシャルに切り出して、data-controllerの宣言をしていきます。

<h1>index ページです</h1>
<div
    data-controller="video-chats-loader"
    data-video-chats-loader-keyword-value="<%= @keyword %>"
    data-video-chats-loader-url-value="<%= video_chats_video_path(@video, format: :html) %>"></div>

こっちはパーシャルです。 params: { format: :js } がミソです。formatを上書きすることでページネーションのリンクをクリックするとjsテンプレートを要求します。

<%= paginate video_chats, remote: true, params: { format: :js } %>
<% video_chats.each do |video_chat| %>
  <div>
    <%= video_chat.text %>
  </div>
<% end %>

ページネーションのレスポンスを返すcontrollerは以下の通りです。

def video_chats
  respond_to do |format|
    format.js
    format.html do
      keyword = params[:keyword]
      video = Video.find(params[:id])
      video_chats = video.video_chats.order('timestamp').page(params[:page]).per(60)
      video_chats = (video_chats.where('message LIKE ?', "%#{keyword}%") if keyword.present?)
      render partial: 'videos/video_chats', locals: { video_chats: }
    end
  end
end

format.jsが返すテンプレートは次の通りです。Stimulusjsのcontrollerを呼び出しています。

var page = <%= params[:page] || 1 %>
var element = document.querySelector("[data-controller='video-chats-loader']")
var controller = window.Stimulus.getControllerForElementAndIdentifier(element, "video-chats-loader")
controller.load(page)

これらを繋げることで、 1 2 3 lastと書かれたハイパーリンクをクリックすると、この部分のみを非同期に書き換えるようになります。

所感

冒頭で挙げた昔の実装よりは、DOMの書き換えをcontrollerに閉じ込めることができているので、マシではありますが、Stimulusjsのcontrollerを呼び出すためにformat.jsを経由してしまっているのが微妙だなと思います。
それと、コードを追いにくい。Stimulusjsコンポーネントから間接的に、パーシャルがレンダリングされると普通は読めない。

素直にturboを使いましょう!!!!!!

参考にした実装

以上。

Discussion