👀
Turboで「もっとみる」ページネーション
以下は
-
@hotwired/turbo
:7.0.0-beta.8
を前提としています。
Turboは、Tubolinks + Stimulusでのパターンが綺麗に取り込まれていて概ね快適です。
ただ、「もっとみる」ページネーションはまだTurbo内で綺麗に実装する道具が揃っていません(問題提起はあったのですが、色々あり無くなってしまいました)。今のTurboで「もっとみる」ページネーションをどう実装するか考えたので記事にしてみました。
現状ではTurbo Framesだけでは内容の全入れ替えにしか対応していません。なのでtarget="append"
なTurbo Streamsが使いたいところですが、Turboが自動でTurbo Streamsのレスポンスを受け入れる(Accept: text/vnd.turbo-stream.html
ヘッダをつける)のは副作用のあるリクエストの場合のみです。この制約のもとでどうするか。
採用した方針
結論から言うと以下のようにしました。
- 自分でJavaScriptから
Accept: text/vnd.turbo-stream.html
をつけたGET
リクエストを投げる- 結果を
Turbo.renderStreamMessage
に投げる
- 結果を
- 2ページ目以降の場合にのみTurbo Streamsのレスポンスを返す
- 副作用のあるリクエストの後に、このエンドポイントにリダイレクトされたときに意図せずTurbo Streamsのレスポンスが帰ってしまうことがあったため
何を使うにしても基本は同じだと思いますが、Railsでのコード例としては以下のような感じです。
Accept: text/vnd.turbo-stream.html
をつけた GET
リクエストを投げる
自分でJavaScriptから app/views/application/_load_more.html.erb
<div
id="load-more"
data-controller="paginated-resource"
>
<%= link_to_next_page page_scope, "もっとみる", data: { action: "paginated-resource#load" } %>
</div>
paginated_resource_controller.ts
import { Controller } from "stimulus";
import { renderStreamMessage } from "@hotwired/turbo";
export default class extends Controller {
element: HTMLDivElement
loading: boolean = false;
async load(e) {
e.preventDefault();
if(this.loading) return;
const message = await this.request(e);
if(message) renderStreamMessage(message);
}
async request(e): Promise<string | null> {
try {
this.loading = true;
// 必要に応じて表示を変える
const response = await fetch(e.target.href, {
headers: {
"Accept": "text/vnd.turbo-stream.html",
},
});
const message = await response.text();
return message;
} catch(e) {
console.error(e);
return null;
} finally {
// 必要に応じて表示を戻す
this.loading = false;
}
}
}
2ページ目以降の場合にのみTurbo Streamsのレスポンスを返す
app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.order(id: :desc).page(params[:page])
respond_with_paginated_stream(page_scope: @posts)
end
#...
private
def respond_with_paginated_stream(action = action_name, page_scope:)
respond_to do |format|
format.html { render action }
format.turbo_stream { render action } if page_scope.current_page != 1
end
end
end
app/views/posts/index.turbo_stream.erb
<%= turbo_stream.append "posts" do %>
<%= render @posts %>
<% end %>
<%= turbo_stream.replace "load-more" do %>
<%= render "application/load_more", page_scope: @posts %>
<% end %>
採用しなかった方針
以下採用しなかった方針も一応書いておきます。
採用しなかった方針1: Turbo Framesの入れ子
How to paginate items using Turbo - DEV Community 👩💻👨💻でのアプローチです。
i
ページを以下のように返していくことでページネーションを実現します。
-
turbo-frame#page-#{i}
- 要素
-
turbo-frame#page-#{i+1}
-
i+1
ページへのリンク
-
非常に面白い!ただ、どんどん深くなる入れ子構造が強制されることが嫌です。例えばリストやテーブルをセマンティックに書くことはできません。
POST /**/page/:page
のような「ページを作成する」エンドポイントにする
採用しなかった方針2: 自然にTurbo Streamsが使えるようにエンドポイント側を変える方針です。
副作用がなくても POST
にしなければいけないし、一般に公開されている場所ではクローラにクローリングして欲しいので選びませんでした。
Discussion