Automatic form submission after user input changes using a debounce mechanism to prevent excessive server requests. Creates a seamless auto-save experience for forms with rich text editors or multiple fields.
63
53%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/form-auto-save/SKILL.mdThe Form Auto Save pattern provides automatic form submission after user input changes, using a debounce mechanism to prevent excessive server requests. This creates a seamless "auto-save" experience for users editing forms.
The pattern uses a Stimulus controller (form-auto-save) that handles the auto-save logic.
Controller Location: app/javascript/controllers/form_auto_save_controller.js
Key Features:
static DEBOUNCE_TIME)change and lexxy:change events (for custom components)cancel() and submit() methods for programmatic controlController Code Pattern:
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static DEBOUNCE_TIME = 8000
connect() {
this.element.addEventListener('change', this.#debounceSubmit.bind(this), { passive: true })
this.element.addEventListener('lexxy:change', this.#debounceSubmit.bind(this), { passive: true })
}
cancel() {
clearTimeout(this.debounceTimer)
}
submit() {
this.element.requestSubmit()
}
#debounceSubmit() {
this.#debounce(this.submit.bind(this))
}
#debounce(callback) {
clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(callback, this.constructor.DEBOUNCE_TIME)
}
}Attach the controller to the form element using Stimulus data attributes.
Required Attributes:
data: { controller: 'form-auto-save' } - Attaches the Stimulus controllerdata: { turbo_permanent: true } - Optional but recommended to preserve form state during Turbo navigationExample (Slim):
= simple_form_for resource, html: { data: { controller: 'form-auto-save', turbo_permanent: true } } do |f|
= f.input :field_name
= f.rich_text_area :contentstatic DEBOUNCE_TIME in the controller if neededchange events (standard HTML input changes)lexxy:change events (custom component events, like rich text editors)turbo_permanent: true keeps the form element across Turbo navigationFor testing auto-save functionality, use the turbo-fetch controller alongside form-auto-save to track request completion without relying on sleep timers.
Add this controller to your JavaScript controllers:
File: app/javascript/controllers/turbo_fetch_controller.js
import { Controller } from '@hotwired/stimulus'
import { patch } from '@rails/request.js'
export default class extends Controller {
static values = {
url: String,
count: Number,
isRunning: { type: Boolean, default: false }
}
async perform({ params: { url: urlParam, query: queryParams } }) {
this.isRunningValue = true
const body = new FormData(this.element)
if (queryParams) Object.keys(queryParams).forEach(key => body.append(key, queryParams[key]))
const response = await patch(urlParam || this.urlValue, { body, responseKind: 'turbo-stream' })
this.isRunningValue = false
if (response.ok) this.countValue += 1
}
}Add this helper to your RSpec support files:
File: spec/support/helpers/turbo_fetch_helper.rb
module TurboFetchHelper
def expect_turbo_fetch_request
count_value = find("[data-controller='turbo-fetch']")['data-turbo-fetch-count-value'] || 0
yield
expect(page).to have_selector("[data-turbo-fetch-count-value='#{count_value.to_i + 1}']")
end
endAdd the turbo-fetch controller alongside form-auto-save:
= simple_form_for resource, html: { data: { controller: 'form-auto-save turbo-fetch', turbo_permanent: true } } do |f|
= f.input :field_name
= f.rich_text_area :contentrequire 'rails_helper'
RSpec.describe 'Form Auto Save', :js do
it 'automatically saves form after changes' do
resource = create(:resource)
visit edit_resource_path(resource)
expect_turbo_fetch_request do
fill_in 'Field name', with: 'Updated value'
end
expect(resource.reload.field_name).to eq('Updated value')
end
it 'debounces multiple rapid changes' do
resource = create(:resource)
visit edit_resource_path(resource)
expect_turbo_fetch_request do
fill_in 'Field name', with: 'First'
fill_in 'Field name', with: 'Second'
fill_in 'Field name', with: 'Final'
end
# Should only save once with final value
expect(resource.reload.field_name).to eq('Final')
end
endCheck:
data: { controller: 'form-auto-save' }change events (text inputs may need blur)Solutions:
DEBOUNCE_TIMESolutions:
turbo_permanent: true to formid attribute097ad6b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.