x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<form class="card flex flex-col gap" data-controller="autosave" data-action="input->autosave#change" action="/slow_action" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="nYKJIwaOK6YSTQk2vjDXd8HMRS39_kpur06udy19v5UKOnMUVMzEra35CLrhKfyDBKBpQBRmou2ysQ37c8YzsQ" autocomplete="off" />
<div class="flex flex-col gap-half">
<h3 class="text-2xl font-semibold leading-none">Autosave</h3>
<p class="text-sm text-subtle">Your changes will be saved automatically without the need to click on the save changes button.</p>
</div>
<div class="flex flex-col gap mb-2">
<div class="flex flex-col gap-half">
<label class="text-sm font-medium leading-none" for="content">Content</label>
<textarea rows="10" class="input" name="content" id="content">
</textarea>
</div>
</div>
<div class="flex items-center">
<button name="button" type="submit" class="btn btn--primary btn--loading" data-autosave-target="submitter"><span>Save changes</span></button>
</div>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%= form_with url: main_app.slow_action_path, class: "card flex flex-col gap", data: { controller: "autosave", action: "input->autosave#change" } do |form| %>
<div class="flex flex-col gap-half">
<h3 class="text-2xl font-semibold leading-none">Autosave</h3>
<p class="text-sm text-subtle">Your changes will be saved automatically without the need to click on the save changes button.</p>
</div>
<div class="flex flex-col gap mb-2">
<div class="flex flex-col gap-half">
<%= form.label :content, class: "text-sm font-medium leading-none" %>
<%= form.text_area :content, rows: 10, class: "input" %>
</div>
</div>
<div class="flex items-center">
<%= form.button tag.span("Save changes"), class: "btn btn--primary btn--loading", data: { autosave_target: "submitter" } %>
</div>
<% end %>
CSS is not required or multiple files are needed, check the notes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { Controller } from "@hotwired/stimulus"
import { FetchRequest } from "https://cdn.skypack.dev/@rails/request.js@0.0.11?min"
const AUTOSAVE_INTERVAL = 3000
export default class extends Controller {
static targets = [ "submitter" ]
#timer
disconnect() {
this.#submit()
}
change() {
!this.#dirty && this.#scheduleSave()
!this.#dirty && this.#updateAppearance()
}
async #submit() {
this.#dirty && await this.#save()
}
async #save() {
this.#updateAppearance(true)
this.#resetTimer()
await this.#submitForm(this.element)
this.#updateAppearance()
}
async #submitForm(form) {
const request = new FetchRequest(form.method, form.action, { body: new FormData(form) })
return await request.perform()
}
#updateAppearance(saving = false) {
if (saving) {
this.element.setAttribute("aria-busy", true)
this.submitterTarget.setAttribute("aria-disabled", true)
this.submitterTarget.disabled = true
} else {
this.element.removeAttribute("aria-busy")
this.submitterTarget.removeAttribute("aria-disabled")
this.submitterTarget.disabled = false
}
}
#scheduleSave() {
this.#timer = setTimeout(() => this.#save(), AUTOSAVE_INTERVAL)
}
#resetTimer() {
clearTimeout(this.#timer); this.#timer = null
}
get #dirty() {
return !!this.#timer
}
}