Hotwireで検索機能を作ってみる

Rails7 でデフォルトとなった`Hotwire`を使って、検索機能を作ってみようと思います。
今回の完成形はこんな感じです。jsは使いません！
![eee.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2322836/4294e854-bd53-edf0-eae9-33f2adf38728.gif)

## Hotwireとは

モダンなWebアプリケーションを構築するための代替アプローチです。
シングルページアプリケーション（以下SPA）を作る時に React や Vue が選択肢に上がると思いますが、それとは別のアプローチを取ります。
以下のような特徴があります。

### Hotwire は複数のライブラリから構成されている

Hotwireは`HTML OVER THE WIRE`の略称で、以下の複数のライブラリをまとめた概念になります。

- Turbo
    - Hotwire の核となるライブラリ
    - Turbolinks の後継者で3つの技術で構成されている
        - Turbo Drive
        - Turbo Frames
        - Turbo Streams
    - 今回は各々の技術の違いを意識しないで機能を作っていきます
- Stimulus
    - 控えめなJavascriptフレームワーク
    - HTML のレンダリングには関与しない
    - 今回はほぼ使わないです
- Strada
    - モバイルアプリ作成用のフレームワーク
    - 近日公開予定

### HTML を中心に置いている

SPAを作るときには、バックエンドから JSON でデータを渡してフロントエンドで HTML を構築することが多いと思います。
Hotwire では JSON ではなく HTML を返します。普通の Rails アプリと同じですが、フロントにいる Turbo が部分書き換えもしてくれるのでSPAのような UX を実現できます。
また HTML を返すので既に構築済みの Rails アプリに Hotwire を適応することも簡単にできます。必要な箇所だけに導入できることは大きなメリットになります。

## 試してみる

それでは検索機能を作っていきます。
検索ボタンをクリックしなくても、フォームに入力した値でユーザーリストをフィルタリングします。
Hotwire を使うと js を書かずに HTML だけで表現できます。

### Hotwire を使わずに作る

まずは Hotwire を使わずに構築していきます。
Railsアプリ作成して、コントローラー、モデルを作ります。
```
$ rails new sample-hotwire -j esbuild
$ cd sample-hotwire

$ bin/rails g controller users index
$ bin/rails g model user name:string
$ bin/rails db:migrate
```

リスト表示するユーザーのデータを作ります。

```ruby:db/seed.rb
User.create([
  { name: "Alice" },
  { name: "Bob" },
  { name: "Carol" },
])
```

```
$ bin/rails db:seed
```

あとはコントローラーでデータ取得、ビューを作ります。

```ruby:app/controllers/users_controller.rb
def index
  @users = if params[:name].present?
             User.where('name LIKE ?', "%#{params[:name]}%")
           else
             User.all
           end
end
```

```erb:app/views/users/index.html.erb
<%= form_with url: users_index_path, method: :get do |form| %>
  <%= form.text_field :name, autocomplete: :off %>
  <%= form.submit :search %>
<% end %>

<% @users.each do |user| %>
  <p>
    <strong>name:</strong>
    <%= user.name %>
  </p>
<% end %>
```

ここで動作確認します。
`http://localhost:3000/users/index`

```
$ bin/dev
```

search ボタンをクリックして、リストをフィルタリングできました。
ここまでは通常の Rails アプリと同じです。
![ccc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2322836/f5facd74-10eb-6370-305b-1ec427f5211d.gif)


### Hotwire を適用する

次に Hotwire を適用していきます。
まずは Stimulus の controller を作ります。
```
$ bin/rails g stimulus form
```

Stimulus のコントローラーで処理を作っていくこともできますが、今回は submit メソッドのみ作成します。
通常の`submit`では Turbo がリクエストをインターセプトしないため`requestSubmit`に変更しています。
```js:app/javascript/controllers/form_controller.js
export default class extends Controller {
  connect() {
  }
  submit() {
    this.element.requestSubmit()
  }
}
```

最後にビューを変更します。

- form_with の変更
    - data-turbo-frame に書き換えたい turbo-frame を指定（`user-list`）
    - data-controller を指定（`form`）
    - data-action を指定（`input->form#submit`）
- list の変更
    - listのみ書き換えたいため`turbo_frame_tag`で囲みます
        - `<turbo-frame></turbo-frame>`に変換されます

```erb:app/views/users/index.html.erb
<%= form_with url: users_index_path,
              method: :get,
              html: { data: {
                turbo_frame: "user-list",
                controller: "form",
                action: "input->form#submit",
              }} do |form| %>
  <%= form.text_field :name, autocomplete: :off %>
  <%= form.submit :search %>
<% end %>

<%= turbo_frame_tag "user-list" do %>
  <% @users.each do |user| %>
    <p>
      <strong>name:</strong>
      <%= user.name %>
    </p>
  <% end %>
<% end %>
```

これだけでフォームに入力したタイミングで、リストのフィルタリングができます。

![eee.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2322836/4294e854-bd53-edf0-eae9-33f2adf38728.gif)

## まとめ

js を使わずにSPAのような UX を実現できました。
React や Vue と比べればやれることの幅や、コードの書き心地はよくないかもしれないです。
ただ小中規模のアプリで少人数のチームであれば、バックエンドに処理を寄せることにより開発スピードとUXの両方を得ることができると思います。
学習コストもそこまで高くないので Turbo と Stimulus でどこまでやれるか試していきたいと思います。

## Reference

https://hotwired.dev/