关于 stimulus
面向 Rails 开发者的 Stimulus 技能,指导通过 HTML 数据属性为服务端渲染页面添加交互。它强调小型、可组合的控制器、渐进增强,以及由 CSS 管理视觉状态。
主要功能
- 01通过数据属性连接 HTML 与控制器
- 02使用 targets、values、classes 和 outlets
- 03组合小型控制器实现交互
- 04管理生命周期并清理资源
- 05用 CSS 类控制视觉状态
- 06调试连接、目标和动作
适用场景
- 01为服务端渲染页面添加切换与弹窗
- 02配置表单、搜索或剪贴板交互
- 03排查控制器连接或动作未触发
- 04组织控制器并实现跨控制器通信
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill stimulus安装后在 Claude Code 中输入/stimulus确认已加载。
附带文件reference.md
详细介绍
Rails Stimulus 专家
构建小巧、专注的 JavaScript 控制器,通过数据属性将 HTML 与行为连接起来。
理念
核心原则:
- HTML 优先 — Stimulus 为服务器渲染的 HTML 增强功能,而不是取代它
- 小型控制器 — 一个控制器 = 一种行为。将多个控制器叠加到元素上进行组合
- 渐进式增强 — 页面必须在没有 JavaScript 的情况下正常运行;控制器负责添加交互功能
- 不在 JS 中渲染 — 控制器操作 DOM 状态(类、属性、可见性),绝不构建 HTML 字符串
- 约定优于配置 — 数据属性负责连接所有内容;无需手动绑定事件
Stimulus 的心智模型:
HTML (data attributes) → Controller (JS behavior) → DOM changes (classes, text, visibility)
↑ source of truth ↑ small & focused ↑ CSS does the heavy lifting
何时使用此技能
- 创建新的 Stimulus 控制器
- 通过数据属性将控制器连接到 HTML
- 为服务器渲染的视图添加交互功能(切换、模态框、剪贴板、闪现消息、表单)
- 调试控制器连接问题
- 组织控制器文件和导入
- 使用 values、targets、classes、outlets 和生命周期回调
- 通过 outlets 或自定义事件实现控制器间通信
说明
步骤 1:检查现有控制器
创建新控制器前,务必先搜索现有控制器:
# List all controllers
ls app/javascript/controllers/
# Search for similar behavior
rg "static targets" app/javascript/controllers/
rg "static values" app/javascript/controllers/
# Check if there's a matching controller already
rg "data-controller=\"toggle\"" app/views/
遵循项目现有约定 — 命名、风格、模式。保持一致比追求“理想”更重要。
步骤 2:生成或创建控制器
使用 Rails 生成器:
bin/rails generate stimulus example
# Creates: app/javascript/controllers/example_controller.js
# Updates: app/javascript/controllers/index.js (if not using auto-loading)
或手动创建:
// app/javascript/controllers/example_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
}
}
app/javascript/controllers/ 中的控制器会通过 index.js 自动注册:
// app/javascript/controllers/index.js
import { application } from "./application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
步骤 3:定义控制器接口
在静态声明区顶部声明 targets、values、classes 和 outlets:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "output", "submit"]
static values = {
url: String,
count: { type: Number, default: 0 },
enabled: Boolean,
items: Array,
config: Object
}
static classes = ["active", "loading", "hidden"]
static outlets = ["other-controller"]
// Lifecycle, then actions
connect() { }
disconnect() { }
// Action methods
toggle() { }
submit() { }
}
顺序约定: 静态声明 → 生命周期回调 → 动作 → 私有辅助方法。
步骤 4:使用数据属性连接 HTML
⚠️ 关键:数据属性命名是导致错误的首要原因。
规则:
- 控制器名称:HTML 中使用 kebab-case(
data-controller="my-thing"),文件名使用 snake_case(my_thing_controller.js),HTML 中绝不使用 camelCase - 多词 values:HTML 属性中使用 kebab-case,JavaScript 访问时使用 camelCase
- Target 属性格式:
data-{controller}-target="{name}" - Value 属性格式:
data-{controller}-{name}-value="{val}" - Class 属性格式:
data-{controller}-{class}-class="{css-class}" - Action 格式:
data-action="{event}->{controller}#{method}"
<%# Controller with values, targets, and actions %>
<div data-controller="search"
data-search-url-value="<%= search_path %>"
data-search-debounce-value="300"
data-search-active-class="is-active">
<input data-search-target="input"
data-action="input->search#query keydown.escape->search#clear"
type="text"
placeholder="Search...">
<div data-search-target="results"></div>
</div>
Agent 常犯的命名错误:
<%# WRONG — camelCase in HTML attribute %>
<div data-controller="myThing">
<div data-myThing-url-value="/api">
<%# CORRECT — kebab-case in HTML %>
<div data-controller="my-thing">
<div data-my-thing-url-value="/api">
<%# WRONG — wrong target format %>
<div data-target="search.input">
<%# CORRECT — namespaced target format %>
<div data-search-target="input">
步骤 5:正确处理 actions
默认事件(可省略事件名称):
| 元素 | 默认事件 |
|---|---|
<button> | click |
<input> | input |
<select> | change |
<form> | submit |
<a> | click |
<textarea> | input |
<details> | toggle |
<%# These are equivalent for a button: %>
<button data-action="click->toggle#flip">Toggle</button>
<button data-action="toggle#flip">Toggle</button>
<%# Multiple actions on one element: %>
<input data-action="input->search#query focus->search#expand blur->search#collapse">
<%# Keyboard modifiers: %>
<input data-action="keydown.enter->form#submit keydown.escape->form#cancel">
<%# Event options: %>
<a data-action="click->nav#toggle:prevent">Link</a>
<button data-action="click->menu#close:stop">Close</button>
<div data-action="scroll->lazy#load:once">Load once</div>
可用的按键修饰符: enter、tab、esc、space、up、down、left、right、home、end,以及任何 KeyboardEvent.key 值。
Action 选项: :prevent(preventDefault)、:stop(stopPropagation)、:once(首次调用后移除)、:self(仅当 event.target 就是元素本身时触发)。
步骤 6:使用生命周期回调
export default class extends Controller {
// Called once when controller class is first instantiated
// Use for: one-time setup like binding methods for callbacks
initialize() {
this.search = this.search.bind(this)
}
// Called every time the controller's element enters the DOM
// Use for: setting up timers, observers, fetching initial data
connect() {
this.interval = setInterval(() => this.poll(), 5000)
}
// Called every time the controller's element leaves the DOM
// Use for: cleanup! Timers, observers, event listeners
disconnect() {
clearInterval(this.interval)
}
// Target connected/disconnected callbacks
outputTargetConnected(element) {
// Called when a new output target appears in DOM
}
outputTargetDisconnected(element) {
// Called when an output target is removed from DOM
}
// Value change callbacks
countValueChanged(newValue, oldValue) {
this.outputTarget.textContent = newValue
}
}
⚠️ 始终在 disconnect() 中清理。 Stimulus 控制器会随着 DOM 变化而连接或断开(Turbo 导航、Turbo Streams 等)。未清理的计时器和观察器是最常见的 Stimulus 错误。
步骤 7:保持控制器小巧
每个控制器只负责一种行为。通过叠加控制器进行组合。
<%# Good — two focused controllers %>
<div data-controller="dropdown tooltip">
<button data-action="click->dropdown#toggle mouseenter->tooltip#show mouseleave->tooltip#hide">
Options
</button>
</div>
<%# Bad — one mega-controller doing everything %>
<div data-controller="dropdown-with-tooltip-and-keyboard-nav">
如果控制器超过约 80 行,它很可能承担了过多职责。 将其拆分。
步骤 8:使用 CSS 表示视觉状态
控制器切换类。CSS 负责渲染。
// Good — controller manages state
toggle() {
this.element.classList.toggle(this.activeClass)
}
// Bad — controller manages appearance
toggle() {
this.element.style.display = this.element.style.display === "none" ? "block" : "none"
this.element.style.opacity = "1"
this.element.style.transform = "translateY(0)"
}
/* CSS handles all visual transitions */
.dropdown { display: none; }
.dropdown.is-active { display: block; }
步骤 9:使用 Outlets 进行跨控制器通信
Outlets 可让一个控制器引用另一个控制器并调用其方法:
// tabs_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static outlets = ["panel"]
select(event) {
const index = event.currentTarget.dataset.index
this.panelOutlets.forEach((panel, i) => {
panel.toggle(i === parseInt(index))
})
}
}
<div data-controller="tabs" data-tabs-panel-outlet=".tab-panel">
<button data-action="click->tabs#select" data-index="0">Tab 1</button>
<button data-action="click->tabs#select" data-index="1">Tab 2</button>
<div class="tab-panel" data-controller="panel">Content 1</div>
<div class="tab-panel" data-controller="panel">Content 2</div>
</div>
**Outlet 命名:**在 data-{controller}-{outlet-name}-outlet 属性中使用 kebab-case 控制器名称。Outlet 的值是一个 CSS 选择器,用于匹配带有目标控制器的元素。
替代方案:自定义事件 —— 当 Outlet 耦合过紧时,可使用这种更松散的方式:
// Dispatching controller
this.dispatch("selected", { detail: { index: 0 } })
// Listening in HTML
<div data-action="tabs:selected->panel#activate">
步骤 10:调试
// Enable debug mode in browser console
Stimulus.debug = true
// Shows: connect/disconnect events, action dispatches, value changes
// Add logging in connect() for troubleshooting
connect() {
console.log(`${this.identifier} connected`, this.element)
console.log("targets:", this.outputTargets)
console.log("values:", this.urlValue, this.countValue)
}
常见问题:
- 控制器未连接 → 检查:
data-controller中是否有拼写错误、文件命名是否正确(snake_case_controller.js)、控制器是否已在index.js中注册 - 未找到 Target → 检查:Target 元素是否位于控制器元素内部,
data-{controller}-target格式是否正确 - Action 未触发 → 检查:
data-action格式是否为event->controller#method,方法是否存在且没有拼写错误 - Value 未更新 → 检查:
data-{controller}-{name}-value格式是否正确,值类型是否与静态声明匹配 - 控制器意外断开 → Turbo 导航替换了 DOM。确保控制器元素保持不变或能正确重新连接。
快速参考
访问 Targets
this.outputTarget // First matching target (throws if missing)
this.outputTargets // Array of all matching targets
this.hasOutputTarget // Boolean — does at least one exist?
访问 Values
this.urlValue // Get
this.urlValue = "/new" // Set (triggers valueChanged callback)
this.hasUrlValue // Boolean — was it specified in HTML?
访问 Classes
this.activeClass // Single class string, e.g. "is-active"
this.activeClasses // Array of classes
this.hasActiveClass // Boolean
访问 Outlets
this.panelOutlet // First matching outlet controller
this.panelOutlets // Array of all matching outlet controllers
this.hasPanelOutlet // Boolean
this.panelOutletElement // The DOM element of the first outlet
this.panelOutletElements // Array of DOM elements
Value 类型
| 类型 | HTML 示例 | JS 默认值 |
|---|---|---|
String | data-x-name-value="hello" | "" |
Number | data-x-count-value="5" | 0 |
Boolean | data-x-open-value="true" | false |
Array | data-x-items-value='["a","b"]' | [] |
Object | data-x-config-value='{"k":"v"}' | {} |
文件组织
app/javascript/
├── application.js # Entry point, imports controllers
├── controllers/
│ ├── application.js # Base controller (extend this)
│ ├── index.js # Auto-loader registration
│ ├── clipboard_controller.js # Simple, focused controllers
│ ├── dropdown_controller.js
│ ├── flash_controller.js
│ ├── modal_controller.js
│ ├── toggle_controller.js
│ └── form_validation_controller.js
命名:{behavior}_controller.js —— 按其行为命名,而不是按其用途命名。
- ✅
toggle_controller.js、clipboard_controller.js、auto_submit_controller.js - ❌
sidebar_controller.js、header_controller.js、user_form_controller.js
常见模式
完整实现请参见 reference.md:
- 带视觉反馈的剪贴板复制
- 自动消失的 Flash 消息
- 模态对话框(使用
<dialog>) - 切换/展开收起
- 表单验证
- 防抖搜索
- 字符计数器
- 自动提交表单
- 嵌套/命名空间控制器
应避免的反模式
- 大型控制器 —— 如果超过 80 行,请将其拆分为可组合的小块
- 在 JS 中渲染 HTML —— 使用 Turbo Streams 处理动态内容;Stimulus 只负责切换状态
- 直接操作样式 —— 切换类,让 CSS 处理外观
- 忘记在断开连接时清理 —— 在
connect()中使用的每个setInterval、addEventListener、MutationObserver都需要在disconnect()中清理 - 在 HTML 属性中使用 camelCase —— 始终使用 kebab-case:
data-my-thing-url-value,而不是data-myThing-url-value - 访问控制器元素之外的内容 —— 使用 outlets 或事件进行跨控制器通信,不要在控制器内部使用
document.querySelector - 在控制器中编写业务逻辑 —— 保持控制器精简;复杂逻辑应放在服务器端
- 未使用 values 配置 —— 不要对 URL、时长或阈值进行硬编码;使用 values,以便通过 HTML 配置行为