コード例
ここで作成するのはリアルタイム検索です。下記のようなUIです。
デモはこちらに用意しています。
<a>タグや<form>タグのネイティブな動作だけでは不十分です。検索窓(<input>タグ)のinputイベントを捉えないとリアルタイム検索に検索してくれません<input>タグのinputイベントを捉える処理を書く必要があります<input>タグのinputイベントを受け取り、そのまま<form>タグのsubmitをするだけですので、ステートを持つ必要はありません<form>タグのsubmitまでが責務です。したがって制御範囲は<form>タグだけで十分であり、検索結果を制御する必要はありません<% content_for :title, "Customers" %> <div class="max-w-lg mx-auto"> <div class="mb-16"> <h1 class="text-4xl text-center">Customers</h1> </div> <%= render "search" %> <%= turbo_frame_tag "customers" do %> <table class="table table-striped w-full"> <thead> <tr class="border-b-2 border-gray-900"> <th class="p-2 text-left">Name</th> <th class="p-2 text-left">JP Name</th> </tr> </thead> <tbody> <% @customers.each do |customer| %> <tr class="group border-t border-gray-400 [:first-child]:border-none"> <td class="p-2"> <%= customer.name %> </td> <td class="p-2"> <%= customer.jp_name %> </td> </tr> <% end %> </tbody> </table> <% end %> </div>
search partialで分けています<turbo-frame id="customers>を設置しています
<div class="max-w-72 mx-auto mb-10"> <%= form_with url: customers_path, method: :get, class: "group", data: {controller: "autosubmit", autosubmit_wait_value: 300, turbo_frame: "customers"} do %> <div class="mt-2"> <%= search_field_tag :query, params[:query], class: "group-aria-busy:bg-[url('/Rolling@1x-1.4s-200px-200px.svg')] bg-contain bg-no-repeat bg-[left_0_top_0] block w-full rounded-full border-0 pr-4 pl-10 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 sm:text-sm/6", placeholder: "検索", data: { action: "input->autosubmit#submitWithDebounce" } %> </div> <% end %> </div>
data-controller="autosubmit"属性のところでautosubmit Stimulus Controllerと接続しています
data-autosubmit-wait-value="300"属性ではリアルタイム検索をするときのdebounceの待ち時間を設定しています data-turbo-frame="customers"属性により、サーバからのレスポンスは<turbo-frame id="customers"> Turbo Frameのところに入れように指示していますsearch_field_tagは検索窓の<input type="search">を作りますが、そこにはdata-action="input->autosubmit#submitWithDebounce"属性がついています
inputタグのinputイベントを受け取ると、autosubmit Stimulus ControllerのsubmitWithDebounce()が呼ばれる仕組みになっています<form>属性および<turbo-frame>にaria-busy属性を自動的につけます
group-aria-busy:bg-[url('/Rolling@1x-1.4s-200px-200px.svg')]のところでaria-busyをCSS擬似セレクタによって検出し、pending UI(待ちUI)を表示していますimport {Controller} from "@hotwired/stimulus" // Connects to data-controller="autosubmit" export default class extends Controller { static values = {wait: {type: Number, default: 300}} connect() { this.form = this.element this.timeoutId = null } submit() { this.form.requestSubmit() } submitWithDebounce() { console.log("submitWithDebounce") clearTimeout(this.timeoutId) this.timeoutId = setTimeout(() => this.submit(), this.waitValue) } }
static values =ではdebounce処理の待ち時間(wait)を設定しています。デフォルトは300msですが、HTML属性のdata-autosubmit-wait-value="..."を設定すれば自由に変えられますsubmit()がメインの処理です。やっていることはformに対してrequestSubmit()を呼んでいるだけですsubmitWithDebounce()はsubmit()にdebounce処理を追加したものです<a>タグのクリックや<form>内の<button>押下には反応します。しかし今回はinputイベントに応答しますのでStimulusを使わなければなりません<input>タグのフォーカス)realtime-searchのようにせず、最初からautosubmitにしていますが、これは再利用性が予見できたためです。検索以外の用途でも使えるような名前にしています
Next.jsはversion 15になって、非同期通信でクライアントサイドナビゲーションをする<code><Form></code>コンポーネントを用意しました。一方でHotwireは当初から<form>でGETリクエストをするようにできており、前身のUJS (Unobtrusive JavaScript)の頃からこの機能を用意しています。
Hotwireは<input>タグにdata-turbo-submits-withなどでpending UI(待ちUI)をつけられたり、disabled属性が自動的についたり、さらに<form>要素に自動的にaria-busyがついたりするなど、自動でやってくれる範囲が広いです。React/Next.jsであれば新しい<Form>要素を使う上に、useFormStatus()等を使う必要があります。
さすがにBasecampプロジェクト管理システムやHey電子メールシステムで育っただけあって、Next.jsと比較した場合、CRUD周りの機能にはHotwireに一日の長があると言えそうです。