83
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rails7(Hotwire)のSPA体験が快適すぎるので紹介する

Last updated at Posted at 2022-05-09

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をインストール

Gemfile
gem "ransack" # 検索をいい感じにしてくれる
gem "kaminari" # ページネーションをいい感じにしてくれる

controllerを実装

scaffoldを元にして作っていきます。
ここら辺は、通常の実装と同じですね。
1ページあたりの表示数はコントロールできるようにして置きます。(params[:per])

foods_controller.rb
def index
  @q = Food.ransack(params[:q])
  @foods = @q.result.includes(:kind).page(params[:page]).per(params[:per] || 6)
end

viewを実装

views/foods/index.html
  <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>
views/foods/_food.html
<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が書き変わっていたり、左上のハンバーガーがピクピクしているのがわかります。
ezgif-1-fda79523cd.gif

Hotwire(TurboFrame)を使って、SPA化していく!!

事前準備できたので、 SPA化していきます。とても簡単です。

viewsに2箇所追記します。

  • 追加① turbo_frame_tag :hogehoge で囲むことによって、差分更新の範囲を指定します。
  • 追加② data: { turbo_frame: :hogehoge } を追加して、更新対象にリンクさせます。
    hogehogeの部分はわかりやすい名前でOK
views/foods/index.html
//   <------------------- (追加①) 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 %>

以下の部分が対象となりました。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3138323939342f65316334333165652d343531332d353834392d316161662d3030396236653232316266612e706e67.png

ページネーションも対応させます。
同じく、 data: { turbo_frame: :hogehoge } を追加すればOKです。

views/kaminari/_page.erb
<% 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ではわかりにくいですが、動作はかなり軽快になっています。
ezgif-1-93f2f0fdb4.gif

地味に画期的! 遅延読み込み loading: "lazy"

turbo_frame_tagloading: "lazy"の属性を追加することで、遅延読み込みが可能になります。
これがかなり便利で、使い方も簡単です。

例えば以下のように、 kindの詳細ページ/kinds/1で Kindに属しているFoodを一覧で表示したい場合に活躍します。
TrialRais7 2022-05-09 18-32-25.png

Food一覧を遅延読み込みを使って、表示する

turbo_frame_tag :hogehoge, src: '/hoge', loading: "lazy" を読み込みたい箇所に配置するだけです。
ブロックの中身はローディング中に表示したいものを書きます。

今回は、srcにq: { kind_id_eq: @kind.id }, per: 100を指定することで、データをクエリしています。

views/kinds/show.html.erb
<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をつけます

views/foods/_food.html
    <%# =========================== 検索結果/一覧 =========================== %>
    <%# <------------------- (追加) 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からデータを取得して、一覧表示しているのがわかります。
ezgif-1-f1815202c3.gif

最後に

すごいことは、ここまでやってJSのコードがゼロということです。
これを、Vue,Reactなどでやろうものなら、かなり大変で、
コード量はサーバーサイド、フロントエンド共にそれなりに増えます。
ページネーションまでやるとかなり面倒です。

turbo_frame_tagを適切に書くだけで、済むのは本当にすごい。
みなさま是非お試しください。

今回のソースはこちらです。

参考

83
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
83
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?