form-auto-save

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Form 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 (
form-auto-save
) that handles the auto-save logic.
Controller Location:
app/javascript/controllers/form_auto_save_controller.js
Key Features:
  • Debounce time of 8 seconds (configurable via
    static DEBOUNCE_TIME
    )
  • Listens to both
    change
    and
    lexxy:change
    events (for custom components)
  • Uses passive event listeners for better performance
  • Provides
    cancel()
    and
    submit()
    methods for programmatic control
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:
  • data: { controller: 'form-auto-save' }
    - Attaches the Stimulus controller
  • data: { turbo_permanent: true }
    - Optional but recommended to preserve form state during Turbo navigation
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数据属性将控制器附加到表单元素上。
必填属性:
  • data: { controller: 'form-auto-save' }
    - 附加Stimulus控制器
  • data: { turbo_permanent: true }
    - 可选但推荐,用于在Turbo导航期间保留表单状态
示例(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

Important Considerations

重要注意事项

Debounce Time

防抖时间

  • Default: 8 seconds (8000ms)
  • Adjust via
    static DEBOUNCE_TIME
    in the controller if needed
  • Consider user experience: too short = excessive requests, too long = lost changes
  • 默认值:8秒(8000毫秒)
  • 若需调整,可修改控制器中的
    static DEBOUNCE_TIME
  • 需兼顾用户体验:时间过短会导致请求过多,时间过长可能会丢失变更

Event Listeners

事件监听器

  • Listens to
    change
    events (standard HTML input changes)
  • Listens to
    lexxy:change
    events (custom component events, like rich text editors)
  • Uses passive listeners for better scroll performance
  • 监听
    change
    事件(标准HTML输入变更)
  • 监听
    lexxy:change
    事件(自定义组件事件,如富文本编辑器)
  • 使用被动监听器提升滚动性能

Turbo Permanent

Turbo Permanent

  • turbo_permanent: true
    keeps the form element across Turbo navigation
  • Prevents loss of unsaved changes when user navigates
  • Critical for forms with auto-save to maintain debounce timers
  • turbo_permanent: true
    可在Turbo导航时保留表单元素
  • 防止用户导航时丢失未保存的变更
  • 对于带有自动保存功能的表单,保留防抖计时器至关重要

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
turbo-fetch
controller alongside
form-auto-save
to track request completion without relying on sleep timers.
为测试自动保存功能,可将
turbo-fetch
控制器与
form-auto-save
配合使用,无需依赖睡眠计时器即可跟踪请求完成情况。

Turbo Fetch Controller

Turbo Fetch控制器

Add this controller to your JavaScript controllers:
File:
app/javascript/controllers/turbo_fetch_controller.js
javascript
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.js
javascript
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.rb
ruby
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.rb
ruby
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

View Integration for Testing

测试用视图集成

Add the
turbo-fetch
controller alongside
form-auto-save
:
slim
= 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-fetch
控制器与
form-auto-save
一起添加:
slim
= 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

System 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
end
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

    # 应仅保存一次,且为最终值
    expect(resource.reload.field_name).to eq('Final')
  end
end

Common Issues

常见问题

Issue: Form doesn't auto-save

问题:表单无法自动保存

Check:
  • Controller properly attached:
    data: { controller: 'form-auto-save' }
  • Form fields trigger
    change
    events (text inputs may need blur)
  • 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
    turbo_permanent: true
    to form
  • Ensure form has stable
    id
    attribute
  • 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

参考资料