form-auto-save
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseForm Auto Save Skill
表单自动保存Skill
Overview
概述
The 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.
表单自动保存模式会在用户输入变更后自动提交表单,同时使用防抖机制避免过多的服务器请求。这为编辑表单的用户带来了无缝的“自动保存”体验。
When to Use
适用场景
- Long-form editing interfaces where users expect automatic saving
- Forms with rich text editors or multiple fields
- Edit pages where users might navigate away and expect changes to persist
- Forms that benefit from progressive saving without explicit "Save" button clicks
- 用户期望自动保存的长表单编辑界面
- 带有富文本编辑器或多字段的表单
- 用户可能会导航离开且希望变更能持久保存的编辑页面
- 无需点击明确的“保存”按钮、可渐进式保存的表单
Implementation
实现方式
1. Stimulus Controller
1. Stimulus控制器
The pattern uses a Stimulus controller () that handles the auto-save logic.
form-auto-saveController Location:
app/javascript/controllers/form_auto_save_controller.jsKey Features:
- Debounce time of 8 seconds (configurable via )
static DEBOUNCE_TIME - Listens to both and
changeevents (for custom components)lexxy:change - Uses passive event listeners for better performance
- Provides and
cancel()methods for programmatic controlsubmit()
Controller Code Pattern:
javascript
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)
}
}该模式使用一个Stimulus控制器()来处理自动保存逻辑。
form-auto-save控制器位置:
app/javascript/controllers/form_auto_save_controller.js核心特性:
- 8秒防抖时间(可通过配置)
static DEBOUNCE_TIME - 监听和
change事件(针对自定义组件)lexxy:change - 使用被动事件监听器提升性能
- 提供和
cancel()方法用于程序化控制submit()
控制器代码示例:
javascript
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)
}
}2. View Integration
2. 视图集成
Attach the controller to the form element using Stimulus data attributes.
Required Attributes:
- - Attaches the Stimulus controller
data: { controller: 'form-auto-save' } - - Optional but recommended to preserve form state during Turbo navigation
data: { turbo_permanent: true }
Example (Slim):
slim
= simple_form_for resource, html: { data: { controller: 'form-auto-save', turbo_permanent: true } } do |f|
= f.input :field_name
= f.rich_text_area :content使用Stimulus数据属性将控制器附加到表单元素上。
必填属性:
- - 附加Stimulus控制器
data: { controller: 'form-auto-save' } - - 可选但推荐,用于在Turbo导航期间保留表单状态
data: { turbo_permanent: true }
示例(Slim模板):
slim
= simple_form_for resource, html: { data: { controller: 'form-auto-save', turbo_permanent: true } } do |f|
= f.input :field_name
= f.rich_text_area :contentImportant Considerations
重要注意事项
Debounce Time
防抖时间
- Default: 8 seconds (8000ms)
- Adjust via in the controller if needed
static DEBOUNCE_TIME - Consider user experience: too short = excessive requests, too long = lost changes
- 默认值:8秒(8000毫秒)
- 若需调整,可修改控制器中的
static DEBOUNCE_TIME - 需兼顾用户体验:时间过短会导致请求过多,时间过长可能会丢失变更
Event Listeners
事件监听器
- Listens to events (standard HTML input changes)
change - Listens to events (custom component events, like rich text editors)
lexxy:change - Uses passive listeners for better scroll performance
- 监听事件(标准HTML输入变更)
change - 监听事件(自定义组件事件,如富文本编辑器)
lexxy:change - 使用被动监听器提升滚动性能
Turbo Permanent
Turbo Permanent
- keeps the form element across Turbo navigation
turbo_permanent: true - Prevents loss of unsaved changes when user navigates
- Critical for forms with auto-save to maintain debounce timers
- 可在Turbo导航时保留表单元素
turbo_permanent: true - 防止用户导航时丢失未保存的变更
- 对于带有自动保存功能的表单,保留防抖计时器至关重要
Form Validation
表单验证
- Ensure backend validation handles partial saves gracefully
- Consider whether all fields should be required or allow partial completion
- Provide clear error feedback if auto-save fails
- 确保后端验证能优雅处理部分保存的情况
- 考虑是否所有字段都为必填项,或允许部分完成
- 若自动保存失败,需提供清晰的错误反馈
Testing
测试
For testing auto-save functionality, use the controller alongside to track request completion without relying on sleep timers.
turbo-fetchform-auto-save为测试自动保存功能,可将控制器与配合使用,无需依赖睡眠计时器即可跟踪请求完成情况。
turbo-fetchform-auto-saveTurbo Fetch Controller
Turbo Fetch控制器
Add this controller to your JavaScript controllers:
File:
app/javascript/controllers/turbo_fetch_controller.jsjavascript
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
}
}将此控制器添加到你的JavaScript控制器中:
文件路径:
app/javascript/controllers/turbo_fetch_controller.jsjavascript
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
}
}Turbo Fetch Helper
Turbo Fetch辅助工具
Add this helper to your RSpec support files:
File:
spec/support/helpers/turbo_fetch_helper.rbruby
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
end将此辅助工具添加到你的RSpec支持文件中:
文件路径:
spec/support/helpers/turbo_fetch_helper.rbruby
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
endView Integration for Testing
测试用视图集成
Add the controller alongside :
turbo-fetchform-auto-saveslim
= 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 :content将控制器与一起添加:
turbo-fetchform-auto-saveslim
= 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 :contentSystem Spec Example
系统测试示例
ruby
require '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
endruby
require '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
# 应仅保存一次,且为最终值
expect(resource.reload.field_name).to eq('Final')
end
endCommon Issues
常见问题
Issue: Form doesn't auto-save
问题:表单无法自动保存
Check:
- Controller properly attached:
data: { controller: 'form-auto-save' } - Form fields trigger events (text inputs may need blur)
change - Network requests in browser DevTools
排查项:
- 控制器是否正确附加:
data: { controller: 'form-auto-save' } - 表单字段是否触发事件(文本输入可能需要失去焦点才会触发)
change - 浏览器开发者工具中的网络请求情况
Issue: Too many requests
问题:请求过多
Solutions:
- Increase
DEBOUNCE_TIME - Check for unnecessary event triggers
- Verify debounce logic is working
解决方案:
- 增大的值
DEBOUNCE_TIME - 检查是否存在不必要的事件触发
- 验证防抖逻辑是否正常工作
Issue: Lost changes on navigation
问题:导航时丢失变更
Solutions:
- Add to form
turbo_permanent: true - Ensure form has stable attribute
id - Consider adding "unsaved changes" warning
解决方案:
- 为表单添加属性
turbo_permanent: true - 确保表单拥有稳定的属性
id - 考虑添加“未保存变更”提示
Related Patterns
相关模式
- Turbo Streams: For more complex form updates and partial page replacements
- Stimulus Values: If you need per-instance debounce times
- Form Validation: Consider inline validation with auto-save
- Turbo Streams: 用于更复杂的表单更新和部分页面替换
- Stimulus Values: 若需要为每个实例设置不同的防抖时间
- 表单验证: 可结合自动保存实现内联验证
References
参考资料
- Stimulus Controller API: https://stimulus.hotwired.dev/
- Turbo Permanent: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads
- Stimulus控制器API:https://stimulus.hotwired.dev/
- Turbo Permanent:https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads