「Hotwire morphing」の習作

Hotwire morphingを使って一覧表示画面に検索機能を追加してみました。

環境
Ruby 3.3.0
Rails 7.1.3.2
turbo-rails 2.0.5

今回は段階的にユーザ体験を改善していきたいと思います。
Hotwireは下記の順できめ細かな処理ができます。

1. Turbo Drive (デフォルト)
2. Morphing
3. Turbo Frames
4. Turbo Streams

使い勝手が良くなる分だけ実装量が増えます。
実務でも費用対効果を考えた段階的な採用がおすすめです。
今回の習作ではMorphingまでを採用しています。

以下の順序で機能を追加します。
1. 一覧表示画面を用意する。
2. 検索した文字を保持する。
3. 自動でフォームを送信する。

完成画面

先ず一覧表示するアプリを用意します。


rails new search_app
cd search_app
bin/rails generate scaffold item name
bin/rails db:migrate

db/seeds.rb
["A1", "a2", "B1", "b2"].each do |item_name|
  Item.find_or_create_by!(name: item_name)
end

bin/rails db:seed
bin/rails server
open http://127.0.0.1:3000/items

一覧表示できるようになりました。

準備ができたので、検索機能を追加します。 URLの引数nameに値が存在すれば、検索した結果を返すようにします。


app/controllers/items_controller.rb
  def index
    @items = Item.all.order(created_at: :desc)
    @items = Item.where("name LIKE ?", "%#{params[:name]}%") if params[:name].present?
  end

検索フォームを追加します。
ブラウザの履歴で戻れないようにフォーム送信時にturbo_action: :replaceを設定してURLを置き換えます。


app/views/items/index.html.erb
<%= form_with url: items_path, method: :get, data: { turbo_action: :replace } do |form| %>
  <%= form.search_field :name %>
  <%= form.submit "Search" %>
<% end %>

検索できるようになりました。

Turbo Driveはbodyタグを置き換えるので、検索すると入力した文字が消えてしまいます。
少しだけ使いやすくなるように検索フォームの文字が残るようにします。

Morphingはdata-turbo-permanent属性があるタグを置き換えません。


app/views/layouts/application.html.erb
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= yield :head %>

app/views/items/index.html.erb
<%= form_with url: items_path, method: :get, data: { turbo_action: :replace, turbo_permanent: "" } do |form| %>
  <%= form.search_field :name %>
  <%= form.submit "Search" %>
<% end %>

検索フォームの文字が残るようになりました。

data-turbo-permanentタグにIDが付けるとページ遷移で戻った時にもタグが残るようになります。
今回の習作アプリの場合、戻った時に文字が残るとURLと一致しなくなりますのでMorphing時だけタグが残るようにIDを付けません。

最後に送信ボタンを押さずに自動でフォームを送信するようにします。
自動送信のコントローラautosubmitを作成します。


bin/rails g stimulus autosubmit

app/javascript/controllers/autosubmit_controller.js
export default class extends Controller {
  submit() {
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
      this.element.requestSubmit()
    }, 300)
  }
}

フォームをautosubmitコントローラと接続します。
テキストフィールドのアクションにautosubmitコントローラのsubmitメソッドを設定します。
送信ボタンを削除します。


app/views/items/index.html.erb
<%= form_with url: items_path, method: :get, data: { turbo_action: :replace, turbo_permanent: "", controller: "autosubmit" } do |form| %>
  <%= form.search_field :name, data: { action: "autosubmit#submit" } %>
<% end %>

完成しました。