自動送信フォーム
stimeo--auto-submit
input/change をデバウンスし、Turbo でフォームを送信する(送信ボタン不要)。
stimeo--auto-submit コントローラは input/change の一定時間後にフォームを送信し、Rails の検索・絞り込みフォームを送信ボタンなしで Turbo 更新します。担うのは送信の起動(デバウンス+ requestSubmit)のみで、送信そのものとバリデーションは Turbo/サーバに委ねます。連打は 1 回のリクエストにまとめられ、on 値は許可リストとして働くため、マークアップで結線したイベント種別を設定で無視できます。フォーカスは一切移動しないため(WCAG 2.2 3.2.2/4.1.3)、自動送信でキャレットが入力欄から飛ぶことはありません。デバウンス待機を data-auto-submit-pending、送信中を aria-busy で示し、stimeo--auto-submit:submit/:done を発火、announce と message を設定すると無言の結果差し替えを共有の stimeo--announcer に橋渡しします。デバウンスタイマと turbo:submit-end リスナは disconnect(Turbo 遷移含む)で破棄します。ライブラリは挙動のみを提供し、見た目はこの Playground が持ちます。
- 東京都
- 大阪府
- 京都府
- 北海道
- 神奈川県
- 愛知県
- 福岡県
- 沖縄県
<%# Markup for the debounced auto-submit demo.
The controller debounces input and then calls form.requestSubmit(). This
Playground has no search backend, so demo.js cancels the native submit, filters
the list in the browser, completes the Turbo lifecycle with a synthetic
turbo:submit-end (so the controller clears aria-busy and emits done), and
announces the result count through a shared stimeo--announcer. %>
<%
items = t("components.auto_submit.demo.items")
count_template = t("components.auto_submit.demo.count_template")
%>
<div class="auto-submit-demo">
<form class="auto-submit-demo__form" role="search" action="#"
data-controller="stimeo--auto-submit"
data-stimeo--auto-submit-debounce-value="400"
data-action="input->stimeo--auto-submit#submit"
data-auto-submit-count="<%= count_template %>">
<label class="auto-submit-demo__label" for="auto-submit-q">
<%= t("components.auto_submit.demo.label") %>
</label>
<input class="demo-input" id="auto-submit-q" type="search" name="q"
autocomplete="off" data-auto-submit-demo="input"
placeholder="<%= t("components.auto_submit.demo.placeholder") %>">
</form>
<ul class="auto-submit-demo__list" data-auto-submit-demo="list">
<% items.each do |item| %>
<li data-auto-submit-value="<%= item.downcase %>"><%= item %></li>
<% end %>
</ul>
<%# Shared announcer: the result count is read out without moving focus. %>
<div data-controller="stimeo--announcer">
<div class="visually-hidden"
data-stimeo--announcer-target="polite" aria-live="polite" aria-atomic="true"></div>
</div>
</div>
/*
* Presentation-only styles for the debounced auto-submit demo.
* The library debounces and triggers the submit; it only reflects state via
* data-auto-submit-pending (debounce window) and aria-busy (in flight). This CSS
* reacts to those hooks and owns the list/result styling.
*/
.auto-submit-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.auto-submit-demo__form {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.auto-submit-demo__label {
font-size: 0.9rem;
font-weight: 600;
}
/* Visualize the debounce window: the input glows while a submit is pending. */
.auto-submit-demo__form[data-auto-submit-pending] .demo-input {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgb(var(--vital-rgb) / 0.2);
}
.auto-submit-demo__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.auto-submit-demo__list li {
padding: 0.4rem 0.6rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-size: 0.95rem;
}
.auto-submit-demo__list li[hidden] {
display: none;
}
// Consumer-side JS for the debounced auto-submit demo (demo-only).
// The controller debounces input then calls form.requestSubmit(). This Playground
// has no search endpoint, so we cancel the native submit, filter the list in the
// browser, finish the Turbo lifecycle with a synthetic turbo:submit-end (so the
// controller clears aria-busy and emits stimeo--auto-submit:done), and announce
// the result count through the shared stimeo--announcer (polite, no focus move).
const form = document.querySelector(".auto-submit-demo__form");
const input = document.querySelector("[data-auto-submit-demo='input']");
const items = Array.from(document.querySelectorAll("[data-auto-submit-value]"));
// Localized "{count} results" template passed from the ERB via a data attribute.
const countTemplate = form?.dataset.autoSubmitCount ?? "{count}";
if (form && input) {
form.addEventListener("submit", (event) => {
event.preventDefault(); // No real navigation in the demo.
const query = input.value.trim().toLowerCase();
let visible = 0;
for (const li of items) {
const match = li.dataset.autoSubmitValue.includes(query);
li.hidden = !match;
if (match) visible += 1;
}
// Complete the Turbo cycle the controller is waiting on (clears aria-busy).
form.dispatchEvent(new CustomEvent("turbo:submit-end"));
// Announce the count so screen-reader users hear the silent result swap.
window.dispatchEvent(
new CustomEvent("stimeo--announcer:announce", {
detail: { message: countTemplate.replace("{count}", String(visible)) },
}),
);
});
}
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--auto-submit"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
form
|
送信対象のフォーム。任意で、無い場合はコントローラ要素を使う。 | data-stimeo--auto-submit-target="form" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
debounce
|
送信までの待機ミリ秒(既定 300)。 | data-stimeo--auto-submit-debounce-value |
on
|
送信を起動するイベント種別(空白区切り、既定 input change)。 |
data-stimeo--auto-submit-on-value |
announce
|
完了を stimeo--announcer に橋渡しするか(既定 false)。 |
data-stimeo--auto-submit-announce-value |
message
|
announce が有効なとき完了時に読み上げる文言(既定は空)。 |
data-stimeo--auto-submit-message-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
submit
|
デバウンスして requestSubmit を予約する。input/change に結線。 |
stimeo--auto-submit#submit |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
submit
|
送信直前に発火。detail に起動した要素を伴う。 |
stimeo--auto-submit:submit |
done
|
turbo:submit-end で発火。detail に任意の message を伴う。 |
stimeo--auto-submit:done |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-auto-submit-pending |
フォーム要素 | デバウンス送信の待機中に付与される。 |
aria-busy |
フォーム要素 | 送信中は "true"(turbo:submit-end で解除)。 |