Rails7を使った管理画面を業務で、2件ほど進めているのですが、
Rails7から搭載されたHotwireによるSPAっぽい開発が良すぎたので紹介します。
特に今回はRansack、Kaminariをつかった検索一覧画面の実装を紹介します。
これを例えば、Rails API + Vue.jsとかでやろうものなら、なかなか大変ですね。(っていうかめんどくないのでやらない)
事前準備
今回使うプロジェクトはこちら
情報
- ruby2.7
- Rails7.0
- css: tailwind css
使用するデータ
シンプルに食べ物を管理するアプリを作りたいと思います
# 食べ物をカテゴライズする種別データ
create_table :kinds do |t|
t.string :name
t.timestamps
end
# 食べ物データ
create_table :foods do |t|
t.string :name
t.references :kind, null: false, foreign_key: true
t.integer :price
t.text :memo
t.boolean :is_deleted
t.datetime :deleted_at
t.timestamps
end
まずは、普通の一覧ページをRansackで普通に作る
gemをインストール
gem "ransack" # 検索をいい感じにしてくれる
gem "kaminari" # ページネーションをいい感じにしてくれる
controllerを実装
scaffoldを元にして作っていきます。
ここら辺は、通常の実装と同じですね。
1ページあたりの表示数はコントロールできるようにして置きます。(params[:per])
def index
@q = Food.ransack(params[:q])
@foods = @q.result.includes(:kind).page(params[:page]).per(params[:per] || 6)
end
viewを実装
<div class="w-full">
<div class="flex justify-between items-center mb-4">
<h1 class="font-bold text-4xl">Foods</h1>
<%= link_to '+ New food', new_food_path, class: "btn btn-outline btn-primary" %>
</div>
<%# =========================== 検索フォーム =========================== %>
<%= search_form_for @q, url: foods_path do |f| %>
<div class="grid grid-cols-6 gap-3">
<div class="col-span-2">
<%= f.search_field :name_cont, class: "input input-bordered input-primary w-full", placeholder: "食べ物" %>
</div>
<div class="col-span-2">
<%= f.submit '検索', class: 'btn btn-outline btn-primary' %>
</div>
</div>
<% end %>
<%# =========================== 検索結果/一覧 =========================== %>
<div id="foods" class="min-w-full">
<div class="grid grid-cols-12 gap-4">
<%= render @foods %>
</div>
</div>
<%# =========================== ページネーション =========================== %>
<%= paginate @foods %>
</div>
<div class="col-span-12 md:col-span-6" id="<%= dom_id food %>">
<div class="card card-compact bg-base-100 shadow-2xl h-full">
<div class="card-body">
<p class="my-5 text-xl">
<%= food.name %>
<span class="badge badge-lg badge-accent badge-outline">
<%= food.kind.name %>
</span>
</p>
<h2 class="mt-2">
<%= "¥ #{food.price}" %>
</h2>
<p>
<%= food.memo %>
</p>
<div class="justify-end card-actions">
<%= link_to 'Edit', edit_food_path(food), class: "btn btn-outline btn-primary" %>
</div>
</div>
</div>
</div>
動かすとこんな感じです。
URLが書き変わっていたり、左上のハンバーガーがピクピクしているのがわかります。
Hotwire(TurboFrame)を使って、SPA化していく!!
事前準備できたので、 SPA化していきます。とても簡単です。
viewsに2箇所追記します。
- 追加①
turbo_frame_tag :hogehoge
で囲むことによって、差分更新の範囲を指定します。 - 追加②
data: { turbo_frame: :hogehoge }
を追加して、更新対象にリンクさせます。
※hogehoge
の部分はわかりやすい名前でOK
// <------------------- (追加①) turbo_frame_tag で囲むことによって、囲んだ箇所が差分更新の対象となります ----------------------
<%= turbo_frame_tag 'ransack_search', target: '_top' do %>
<div class="w-full">
<div class="flex justify-between items-center mb-4">
<h1 class="font-bold text-4xl">Foods</h1>
<%= link_to '+ New food', new_food_path, class: "btn btn-outline btn-primary" %>
</div>
<%# =========================== 検索フォーム =========================== %>
<%= search_form_for @q, url: foods_path do |f| %>
<div class="grid grid-cols-6 gap-3">
<div class="col-span-2">
<%= f.search_field :name_cont, class: "input input-bordered input-primary w-full", placeholder: "食べ物" %>
</div>
<div class="col-span-2">
// <------------------- (追加②) data: { turbo_frame: :hogehoge } を追加 ----------------------
<%= f.submit '検索', class: 'btn btn-outline btn-primary', data: { turbo_frame: 'ransack_search' } %>
</div>
</div>
<% end %>
<%# =========================== 検索結果/一覧 =========================== %>
<div id="foods" class="min-w-full">
<div class="grid grid-cols-12 gap-4">
<%= render @foods %>
</div>
</div>
<%# =========================== ページネーション =========================== %>
<%= paginate @foods %>
</div>
<% end %>
ページネーションも対応させます。
同じく、 data: { turbo_frame: :hogehoge }
を追加すればOKです。
<% if page.current? %>
<%= content_tag :a, page, rel: page.rel, class: 'btn', data: { turbo_frame: 'ransack_search' } %>
<% else %>
<%= link_to page, url, rel: page.rel, class: 'btn btn-outline', data: { turbo_frame: 'ransack_search' } %>
<% end %>
kaminariの独自テンプレートは、以下のコマンドで生成できます。
今回、tailwindは未対応なので、手動で作成しました。
$ rails g kaminari:views default
以上で、SPA化できました。
URLが変わらず、検索やページネーションができているのがわかります。
GIFではわかりにくいですが、動作はかなり軽快になっています。
地味に画期的! 遅延読み込み loading: "lazy"
turbo_frame_tag
に loading: "lazy"
の属性を追加することで、遅延読み込みが可能になります。
これがかなり便利で、使い方も簡単です。
例えば以下のように、 kindの詳細ページ/kinds/1
で Kindに属しているFoodを一覧で表示したい場合に活躍します。
Food一覧を遅延読み込みを使って、表示する
turbo_frame_tag :hogehoge, src: '/hoge', loading: "lazy"
を読み込みたい箇所に配置するだけです。
ブロックの中身はローディング中に表示したいものを書きます。
今回は、srcにq: { kind_id_eq: @kind.id }, per: 100
を指定することで、データをクエリしています。
<div class="grid grid-cols-2">
<%# ================== Kind 詳細 ======================= %>
<div class="col-span-1">
:
:
:
</div>
<%# ================== Kind に属するFood一覧 ======================= %>
<div class="col-span-1">
<div class="mb-2">
<h1 class="font-bold mb-2"> <%= "#{@kind.name}の一覧" %> </h1>
<hr/>
</div>
// <----------------------- 遅延読み込みで /foods から一覧を読み込む -----------------------
<%= turbo_frame_tag :foods, src: foods_path(q: { kind_id_eq: @kind.id }, per: 100), loading: "lazy" do %>
...loading
<% end %>
</div>
</div>
遅延読み込みして、表示したい部分に、 turbo_frame_tag
をつけます
<%# =========================== 検索結果/一覧 =========================== %>
<%# <------------------- (追加) turbo_frame_tag :hogehoge を追加 ---------------------- %>
<%= turbo_frame_tag :foods do %>
<div id="foods" class="min-w-full">
<div class="grid grid-cols-12 gap-4">
<%= render @foods %>
</div>
</div>
<% end %>
すると、以下のように/foods
からデータを取得して、一覧表示しているのがわかります。
最後に
すごいことは、ここまでやってJSのコードがゼロということです。
これを、Vue,Reactなどでやろうものなら、かなり大変で、
コード量はサーバーサイド、フロントエンド共にそれなりに増えます。
ページネーションまでやるとかなり面倒です。
turbo_frame_tag
を適切に書くだけで、済むのは本当にすごい。
みなさま是非お試しください。
今回のソースはこちらです。
参考