Auto Submit Form
stimeo--auto-submit
Submits the form once typing settles, so a search or filter needs no submit button.
Submits the form a moment after typing settles, so a search or filter form no longer needs a submit button. Fast typing collapses into a single request rather than one per keystroke. The submission itself and any validation stay with Turbo and your server; all this owns is the moment to send. Submitting on your behalf never moves the caret out of the field. The waiting window and the in-flight window are each published, so you can style them. When the result swaps in silently, it can pass the news to the shared announcer for you.
- Rails
- Stimulus
- Turbo
- Hotwire
- Importmap
- Propshaft
- Sidekiq
- Kamal
Keyboard
This component has no keyboard interactions of its own.
<%# 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)) },
}),
);
});
}
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--auto-submit"
Targets
| Name | Description | Attribute |
|---|---|---|
form
|
The form to submit; optional — the controller element is used when absent. | data-stimeo--auto-submit-target="form" |
Values
| Name | Description | Attribute |
|---|---|---|
debounce
|
Milliseconds to wait before submitting (default 300). | data-stimeo--auto-submit-debounce-value |
on
|
Space-separated event types that trigger a submit (default input change). |
data-stimeo--auto-submit-on-value |
announce
|
Whether to bridge completion to stimeo--announcer (default false). |
data-stimeo--auto-submit-announce-value |
message
|
Message announced on completion when announce is on (default empty). |
data-stimeo--auto-submit-message-value |
Actions
| Name | Description | Action |
|---|---|---|
submit
|
Schedules a debounced requestSubmit; wired to input/change. |
stimeo--auto-submit#submit |
Events
| Name | Description | Event |
|---|---|---|
submit
|
Fires just before submitting; detail carries the trigger element. |
stimeo--auto-submit:submit |
done
|
Fires on turbo:submit-end; detail carries an optional message. |
stimeo--auto-submit:done |
reconcile
|
Fires when the Turbo cache rewind drops a pending or in-flight submission; detail is {}. |
stimeo--auto-submit:reconcile |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-auto-submit-pending |
Form element | Present while a debounced submit is pending. |
aria-busy |
Form element | "true" while the submit is in flight (cleared on turbo:submit-end). |