Unsaved-Changes Guard
stimeo--dirty-form
Detects form edits and warns before leaving with unsaved changes (unload + Turbo visit).
The stimeo--dirty-form controller snapshots the form's field values on connect, marks the form data-dirty once a value changes (and clears it when values return to the baseline), and — while dirty — guards both a full unload (beforeunload) and a Turbo visit (turbo:before-visit). On a Turbo visit it dispatches a cancelable stimeo--dirty-form:guard event; if a consumer cancels it (or confirmBridge is set) the visit is blocked, otherwise it falls back to a native confirm(message). A successful submit — the markClean action or turbo:submit-end — clears the dirty state. It also dispatches stimeo--dirty-form:dirty when the state flips. Behavior only — it renders no confirmation UI (pair with a Confirm Bridge) and does not persist input (pair with Persist). beforeunload is wired only while dirty, and every listener is removed on disconnect (Turbo navigation included).
<%# Dirty-form demo: edit the field and the form gets data-dirty (shown as a badge).
While dirty, leaving is guarded (beforeunload + Turbo turbo:before-visit → a native
confirm). "Save" calls the markClean action to clear the dirty state (this catalog
has no server, so it stands in for a successful save). The library owns the
data-dirty hook and the guards; this demo only styles the badge. %>
<div class="dirty-form-demo">
<form class="dirty-form" data-controller="stimeo--dirty-form">
<label class="dirty-form__field">
<span><%= t("components.dirty_form.demo.label") %></span>
<input type="text" name="title" value="<%= t('components.dirty_form.demo.value') %>">
</label>
<div class="dirty-form__bar">
<span class="dirty-form__badge"><%= t("components.dirty_form.demo.unsaved") %></span>
<button type="button" class="demo-trigger" data-action="stimeo--dirty-form#markClean">
<%= t("components.dirty_form.demo.save") %>
</button>
</div>
</form>
</div>
/*
* Presentation-only styles for the dirty-form demo.
* The library toggles data-dirty on the form; this CSS reveals the "unsaved" badge
* while that hook is present.
*/
.dirty-form-demo {
max-width: 28rem;
}
.dirty-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.dirty-form__field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.dirty-form__field input {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font: inherit;
}
.dirty-form__bar {
display: flex;
align-items: center;
gap: 0.75rem;
}
/* The badge appears only while the form is dirty. */
.dirty-form__badge {
display: none;
font-size: 0.8rem;
font-weight: 600;
color: var(--accent);
}
.dirty-form[data-dirty] .dirty-form__badge {
display: inline-block;
}
This demo needs no consumer-side JS (the controller handles the behavior).
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--dirty-form"
Values
| Name | Description | Attribute |
|---|---|---|
message
|
Confirmation message used for the native confirm on a Turbo visit. |
data-stimeo--dirty-form-message-value |
confirmBridge
|
Block the Turbo visit (defer the prompt to a Confirm Bridge) instead of confirm. |
data-stimeo--dirty-form-confirm-bridge-value |
Actions
| Name | Action |
|---|---|
markClean
|
stimeo--dirty-form#markClean |
Events
| Name | Description | Event |
|---|---|---|
dirty
|
Fires when the dirty state flips, with detail.dirty. |
stimeo--dirty-form:dirty |
guard
|
Cancelable; fires on a Turbo visit while dirty. preventDefault blocks the visit. |
stimeo--dirty-form:guard |
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-dirty |
Form (root) | Present while the form differs from its connect baseline. |