ここで作成するのはリアルタイム検索です。下記のビデオをご覧ください。
<input type="search">タグのinputイベントに応答して行います。少し特殊なのでStimulus controllerを用意します。<form>タグにrequestSubmit()を送る3<% content_for :title, "Customers" %> <div class="max-w-lg mx-auto"> <div class="mb-16"> <h1 class="text-4xl text-center">Customers</h1> </div> <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> <%= turbo_frame_tag "customers", target: "_top" 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> <th></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> <td class="p-2"> <%= link_to edit_customer_path(customer) do %> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/> </svg> <% end %> </td> </tr> <% end %> </tbody> </table> <% end %> </div>
form_withの中のsearch_field_tagで実装しています。HTMLの<form>タグを<input type="search">に相当します。
data-controller="autosubmit"属性でAutosubmitController(Stimulus)に接続します。data-autosubmit-wait-value="300"属性ではリアルタイム検索をするときのdebounceの待ち時間を設定しています。data-turbo-frame="customers"属性により、サーバから送られてきたHTMLは<turbo-frame id="customers">で指定されたTurbo Frameのところに入れように指示しています。search_field_tag(<input type="search">)にはdata-action="input->autosubmit#submitWithDebounce"属性がついています。
inputイベントに対して、AutosubmitController(Stimulus)のsubmitWithDebounce()メソッドが呼ばれます。search_field_tag(<input type="search">)にはclass="group-aria-busy:bg-[url('/Rolling@1x-1.4s-200px-200px.svg')]"がついています。
<frame>にaria-busy属性をつけますので、CSS擬似セレクタで検出しています。turbo_frame_tag "customers"(<turbo-frame id="customers>)は、サーバから送られて来たHTMLが挿入される箇所です。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() { clearTimeout(this.timeoutId) this.timeoutId = setTimeout(() => this.submit(), this.waitValue) } }
submit()は、AutosubmitController(Stimulus)が接続されたHTML要素のformに対して、requestSubmit()を呼ぶだけです。submitWithDebounce()はsubmit()にdebounce処理を追加したものです。<form>要素に自動的に追加してくれるaria-busy属性を使い、CSSだけでpending UI (待ちUI)を実装しました。<input>タグにdata-turbo-submits-withなどでpending UI(待ちUI)を簡単に追加できたり、何もしなくてもdisabled属性が自動的についたりなど、一般的な処理を最初から付けてくれいます。これもRailsのOmakaseの発想と言えるでしょう。TurboFramesとTurboStreamsのいずれを使用するかの判断基準は別途解説します。 ↩
Reactの場合は最低限でもサーバから送信されたJSON APIのデータをステートに保管しなければなりません。
これはReactが任意のタイミングでコンポーネントを再レンダリングするためで、Hotwireでは不要になります。 ↩
サーバにリクエストを送る際、JavaScriptからTurboFramesやTurboStreamsを使うことも可能です。またもちろんfetch()を使うこともできます。しかしHotwireはネイティブな感覚を重視しますので、<form>を普通にsubmitするように記述します。 ↩