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

Rails7を使った管理画面を業務で、2件ほど進めているのですが、
Rails7から搭載されたHotwireによるSPAっぽい開発が良すぎたので紹介します。

特に今回はRansack、Kaminariをつかった検索一覧画面の実装を紹介します。
これを例えば、Rails API + Vue.jsとかでやろうものなら、なかなか大変ですね。（っていうかめんどくないのでやらない)

# 事前準備
## 今回使うプロジェクトはこちら
https://qiita.com/tikaranimaru/items/70f87b1fe4b1e7166348

## 情報
- ruby2.7
- Rails7.0
- css: tailwind css

## 使用するデータ
シンプルに食べ物を管理するアプリを作りたいと思います
```rb
# 食べ物をカテゴライズする種別データ
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をインストール

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

### controllerを実装
scaffoldを元にして作っていきます。
ここら辺は、通常の実装と同じですね。
1ページあたりの表示数はコントロールできるようにして置きます。(params[:per])
```rb:foods_controller.rb
def index
  @q = Food.ransack(params[:q])
  @foods = @q.result.includes(:kind).page(params[:page]).per(params[:per] || 6)
end
```

### viewを実装

```erb: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>
```

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182994/04fc9648-85e4-c49f-8e45-5bc386be85cd.gif)


# Hotwire(TurboFrame)を使って、SPA化していく！！
事前準備できたので、 SPA化していきます。とても簡単です。

viewsに2箇所追記します。
- 追加① `turbo_frame_tag :hogehoge` で囲むことによって、差分更新の範囲を指定します。
- 追加② `data: { turbo_frame: :hogehoge }` を追加して、更新対象にリンクさせます。
※ `hogehoge`の部分はわかりやすい名前でOK

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182994/7574d591-b4d7-79dd-f123-9bc1ac78bbb3.png)

ページネーションも対応させます。
同じく、 `data: { turbo_frame: :hogehoge }` を追加すればOKです。
```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182994/86558b9f-f74d-48d4-a7f6-b1ca71356397.gif)

# 地味に画期的！ 遅延読み込み `loading: "lazy"`
`turbo_frame_tag`に  `loading: "lazy"`の属性を追加することで、遅延読み込みが可能になります。
これがかなり便利で、使い方も簡単です。

例えば以下のように、 kindの詳細ページ`/kinds/1`で Kindに属しているFoodを一覧で表示したい場合に活躍します。
![TrialRais7 2022-05-09 18-32-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182994/237bb87d-5a2c-2a2c-5fc8-72adc5f0e20c.png)


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

今回は、srcに`q: { kind_id_eq: @kind.id }, per: 100`を指定することで、データをクエリしています。
```erb: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`をつけます
```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182994/4adea065-55f0-37f0-31fa-55ad53e66aac.gif)



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

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

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

https://github.com/tikaranimaru/trial-rais7


# 参考

https://logmi.jp/tech/articles/324253