コード例
ここで作成するのはリアルタイム検索です。下記のような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('/[email protected]')] 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('/[email protected]')]
のところで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になって、非同期通信でクライアントサイドナビゲーションをする<Form>
コンポーネントを用意しました。一方で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に一日の長があると言えそうです。