二重送信防止
stimeo--submit-once
送信ボタンを一度だけ押せるようにします。二重送信を防ぎ、終われば元へ戻ります。
送信を押した瞬間にボタンを押せなくして、二重に送ってしまう事故を防ぎます。処理中であることは読み上げにも伝わります。終われば元に戻ります。Turbo が終わりを教えてくれるほか、自分で終わりを伝えることも、時間切れで戻すこともできます。ボタンの中にアイコンがあっても壊さずに文言だけ差し替えられます。処理中の文言は読み上げにも流せます。Turbo のフォームなら設定は要りません。ページの中を移動しても、Turbo がページを覚えていても、押せないままのボタンやフォーカスの迷子は残りません。
実行中
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Submit-once demo. A structured idle / busy target pair keeps the DOM the
consumer authored while the button is disabled. The busy ↔ ready transition
goes to the shared stimeo--announcer your app seats once, in its layout,
where it is read without moving focus; aria-busy stays state the reader can
query, not a notification. This catalog has no server, so demo.js fires
Turbo's start / end lifecycle around a short fake round trip. %>
<% announce_start = t("components.submit_once.demo.announce_start") %>
<% announce_ready = t("components.submit_once.demo.announce_ready") %>
<% submit_label = t("components.submit_once.demo.submit") %>
<% busy_label = t("components.submit_once.demo.busy") %>
<div class="submit-once-demo">
<form
data-controller="stimeo--submit-once"
data-stimeo--submit-once-announce-text-value="<%= announce_start %>"
data-stimeo--submit-once-announce-ready-text-value="<%= announce_ready %>">
<label class="submit-once-demo__field">
<span><%= t("components.submit_once.demo.label") %></span>
<input type="text" name="title" class="submit-once-demo__input">
</label>
<button type="submit" class="demo-trigger" data-stimeo--submit-once-target="submit">
<span data-stimeo--submit-once-target="idle"><%= submit_label %></span>
<span data-stimeo--submit-once-target="busy" hidden><%= busy_label %></span>
</button>
</form>
</div>
/*
* Presentation-only styles for the submit-once demo.
* The library disables the button, sets aria-busy, swaps the label, and marks the
* form with data-submitting; this CSS only reflects that busy state.
*/
.submit-once-demo {
max-width: 28rem;
}
.submit-once-demo form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.submit-once-demo__field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.submit-once-demo__input {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font: inherit;
}
/* Dim and show a progress cursor on the disabled, busy button. */
.submit-once-demo button[aria-busy="true"] {
opacity: 0.6;
cursor: progress;
}
// Submit-once demo (consumer-side JS).
//
// This catalog has no server, so the demo fires the same two events Turbo would
// around a submission. Every busy-state change belongs to the controller.
document.querySelectorAll(".submit-once-demo form").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
if (form.hasAttribute("data-submitting")) return;
const submitter = event instanceof SubmitEvent ? event.submitter : null;
form.dispatchEvent(
new CustomEvent("turbo:submit-start", {
bubbles: true,
detail: { formSubmission: { formElement: form, submitter } },
}),
);
window.setTimeout(() => {
form.dispatchEvent(
new CustomEvent("turbo:submit-end", { bubbles: true, detail: { success: true } }),
);
}, 1500);
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--submit-once"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
submit
|
任意の明示的な送信コントロール。フォーム標準コントロールは常に自動検出。 | data-stimeo--submit-once-target="submit" |
idle
|
構造化ボタンの通常表示。同じ送信ボタン内の busy と対にする。 | data-stimeo--submit-once-target="idle" |
busy
|
構造化ボタンの送信中表示。初期 hidden で idle と対にする。 | data-stimeo--submit-once-target="busy" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
announceText
|
共有 Announcer へ渡す任意の開始文言。空なら通知しない。 | data-stimeo--submit-once-announce-text-value |
announceReadyText
|
共有 Announcer へ渡す任意の完了文言。 | data-stimeo--submit-once-announce-ready-text-value |
busyLabel
|
安全な plain-text/value/aria-label 差し替えの既定文言。空なら据え置き。 | data-stimeo--submit-once-busy-label-value |
timeout
|
強制復帰までのミリ秒。0 で無効(turbo:submit-end に依存)。 |
data-stimeo--submit-once-timeout-value |
restoreFocus
|
起動ボタンがフォーカスを失い、利用者が他へ移していない場合だけ復帰。 | data-stimeo--submit-once-restore-focus-value |
アクション
| 名前 | アクション |
|---|---|
cancel
|
stimeo--submit-once#cancel |
finish
|
stimeo--submit-once#finish |
start
|
stimeo--submit-once#start |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
start
|
送信フォームと submitter を伴って開始時に発火。 | stimeo--submit-once:start |
end
|
フォーム、submitter、完了理由、任意の success を伴って復帰時に発火。 | stimeo--submit-once:end |
reconcile
|
Turbo キャッシュの巻き戻しが進行中の送信を放棄したとき発火。detail に対象の forms。 |
stimeo--submit-once:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-busy |
送信ボタン | 送信中に "true" を付与。 |
disabled |
送信ボタン | 送信中に付与し二度押しを防ぐ。 |
data-submitting |
フォーム(ルート) | 送信処理中に付与。 |