二重送信防止
stimeo--submit-once
送信時に送信ボタンを無効化して二重送信を防ぎ、完了で復帰する。
stimeo--submit-once コントローラは送信時にフォームの送信ボタンを無効化して二重送信を防ぎ、ボタンに aria-busy、フォームに data-submitting を付与し、起動した送信ボタンのラベルをビジー文言へ差し替えます(ボタン個別の data-submit-once-busy-label がフォーム既定の busyLabel Value を上書き)。復帰は Turbo の turbo:submit-end(ボタンを再有効化しラベルを戻す)か timeout ミリ秒で行い、 restoreFocus でボタンへフォーカスを戻せます。Rails の disable_with の Headless 上位版で、 stimeo--submit-once:start / :end を発火します。ライブラリは挙動のみで、スピナーは描画しません(Spinner と組み合わせ)。submit-end リスナとタイムアウトは disconnect で破棄し、connect でキャッシュ復元由来のビジー状態を解除するため、ボタンが押せないまま固定されません。
<%# Submit-once demo: the submit button disables and swaps its label on submit, then
restores. This catalog has no server, so demo.js intercepts the submit and simulates
a round trip by firing turbo:submit-end after a short delay. The library owns the
disable / aria-busy / data-submitting / label state; this demo only styles it. %>
<div class="submit-once-demo">
<form
data-controller="stimeo--submit-once"
data-stimeo--submit-once-busy-label-value="<%= t('components.submit_once.demo.busy') %>"
data-action="submit->stimeo--submit-once#start">
<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">
<%= t("components.submit_once.demo.submit") %>
</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).
//
// The controller disables the submit button and swaps its label on submit, then
// restores on Turbo's turbo:submit-end. This catalog has no server, so here we
// intercept the submit, prevent the real navigation, and simulate a round trip by
// dispatching turbo:submit-end after a short delay — letting you see the busy →
// restored cycle the controller drives.
document.querySelectorAll(".submit-once-demo form").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
window.setTimeout(() => {
form.dispatchEvent(new CustomEvent("turbo:submit-end", { 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" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
busyLabel
|
フォーム既定のビジー文言。フォームに置く。空なら据え置き。 | data-stimeo--submit-once-busy-label-value |
timeout
|
強制復帰までのミリ秒。0 で無効(turbo:submit-end に依存)。 |
data-stimeo--submit-once-timeout-value |
restoreFocus
|
復帰時に起動ボタンへフォーカスを戻すか。既定 false。 |
data-stimeo--submit-once-restore-focus-value |
アクション
| 名前 | アクション |
|---|---|
start
|
stimeo--submit-once#start |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
start
|
送信開始時に発火。 | stimeo--submit-once:start |
end
|
復帰時に発火。turbo:submit-end があれば detail.success を伴う。 |
stimeo--submit-once:end |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-busy |
送信ボタン | 送信中に "true" を付与。 |
disabled |
送信ボタン | 送信中に付与し二度押しを防ぐ。 |
data-submitting |
フォーム(ルート) | 送信処理中に付与。 |