自動送信フォーム
stimeo--auto-submit
入力が落ち着いたらフォームを送信します。検索や絞り込みに送信ボタンが要らなくなります。
入力が少し落ち着いたところでフォームを送信します。検索欄や絞り込みから送信ボタンをなくせます。連続して打っても送信は 1 回にまとまるので、打鍵のたびにリクエストが飛ぶことはありません。送信そのものと検証は Turbo とサーバに任せ、ここが受け持つのは送信のきっかけだけです。自動で送っても、カーソルは入力欄に残ります。送信を待っている間と送信中は、それぞれ状態を出すので、CSS で待ち表示を書けます。結果が黙って差し替わる場合は、共通の読み上げの窓口へ知らせることもできます。
実行中
- 東京都
- 大阪府
- 京都府
- 北海道
- 神奈川県
- 愛知県
- 福岡県
- 沖縄県
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# 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 the shared stimeo--announcer your app seats
once, in its layout (the result count is read out without moving focus). %>
<%
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>
</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 |
reconcile
|
Turbo キャッシュの巻き戻しが保留中・送信中の送信を捨てたとき発火。detail は {}。 |
stimeo--auto-submit:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-auto-submit-pending |
フォーム要素 | デバウンス送信の待機中に付与される。 |
aria-busy |
フォーム要素 | 送信中は "true"(turbo:submit-end で解除)。 |