import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { url: String }
static targets = ["status"]
connect() {
this.timeout = null
}
queue() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => this.save(), 500)
}
async save() {
if (!this.urlValue) return
this.statusTarget.textContent = "Saving…"
const token = document.querySelector("meta[name='csrf-token']").content
const body = new FormData(this.element)
const res = await fetch(this.urlValue, { method: "PATCH", headers: { "X-CSRF-Token": token }, body })
this.statusTarget.textContent = res.ok ? "Saved" : "Error"
}
}
<%= form_with model: @draft, data: { controller: "autosave", autosave_url_value: draft_path(@draft) } do |f| %>
<span data-autosave-target="status" class="text-xs text-gray-500"></span>
<%= f.text_field :title, data: { action: "input->autosave#queue" } %>
<%= f.text_area :body, data: { action: "input->autosave#queue" } %>
<% end %>
Autosave drafts without fighting Turbo: send a fetch request with CSRF token, keep it separate from the main form submit, and surface “Saved” status unobtrusively. This is a great fit for content editors.