stimulus

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Stimulus

Stimulus

Modest JavaScript framework that connects JS objects to HTML via data attributes. Stimulus does not render HTML -- it augments server-rendered HTML with behavior.
The mental model: HTML is the source of truth, JavaScript controllers attach to elements, and data attributes are the wiring. No build step required with AssetMapper.
一款轻量的JavaScript框架,通过data属性将JS对象与HTML关联起来。Stimulus不会渲染HTML——它会为服务端渲染的HTML增强交互行为。
核心心智模型:HTML是唯一数据源,JavaScript控制器绑定到元素上,data属性是连接两者的纽带。搭配AssetMapper使用时无需构建步骤。

Quick Reference

快速参考

data-controller="name"              attach controller to element
data-name-target="item"             mark element as a target
data-action="event->name#method"    bind event to controller method
data-name-key-value="..."           pass typed data to controller
data-name-key-class="..."           configure CSS class names
data-name-other-outlet=".selector"  reference another controller instance
data-controller="name"              attach controller to element
data-name-target="item"             mark element as a target
data-action="event->name#method"    bind event to controller method
data-name-key-value="..."           pass typed data to controller
data-name-key-class="..."           configure CSS class names
data-name-other-outlet=".selector"  reference another controller instance

Controller Skeleton

控制器骨架

javascript
// assets/controllers/example_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['input', 'output'];
    static values = { url: String, delay: { type: Number, default: 300 } };
    static classes = ['loading'];
    static outlets = ['other'];

    connect() {
        // Called when controller connects to DOM
    }

    disconnect() {
        // Called when controller disconnects -- clean up here
    }

    submit(event) {
        // Action method
    }
}
File naming convention:
hello_controller.js
maps to
data-controller="hello"
. Subdirectories use
--
as separator:
components/modal_controller.js
maps to
data-controller="components--modal"
.
javascript
// assets/controllers/example_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['input', 'output'];
    static values = { url: String, delay: { type: Number, default: 300 } };
    static classes = ['loading'];
    static outlets = ['other'];

    connect() {
        // Called when controller connects to DOM
    }

    disconnect() {
        // Called when controller disconnects -- clean up here
    }

    submit(event) {
        // Action method
    }
}
文件命名规范:
hello_controller.js
对应
data-controller="hello"
。子目录使用
--
作为分隔符:
components/modal_controller.js
对应
data-controller="components--modal"

HTML Wiring Examples

HTML绑定示例

Basic Controller

基础控制器

html
<div data-controller="hello">
    <input data-hello-target="name" type="text">
    <button data-action="click->hello#greet">Greet</button>
    <span data-hello-target="output"></span>
</div>
html
<div data-controller="hello">
    <input data-hello-target="name" type="text">
    <button data-action="click->hello#greet">Greet</button>
    <span data-hello-target="output"></span>
</div>

Values from Server (Twig)

来自服务端的值(Twig)

Pass server data to controllers via value attributes. Values are typed and automatically parsed.
html
<div data-controller="map"
     data-map-latitude-value="{{ place.lat }}"
     data-map-longitude-value="{{ place.lng }}"
     data-map-zoom-value="12">
</div>
Available types:
String
,
Number
,
Boolean
,
Array
,
Object
. Values trigger
{name}ValueChanged()
callbacks when mutated.
通过value属性将服务端数据传递给控制器。值是带类型的,会自动解析。
html
<div data-controller="map"
     data-map-latitude-value="{{ place.lat }}"
     data-map-longitude-value="{{ place.lng }}"
     data-map-zoom-value="12">
</div>
可用类型:
String
Number
Boolean
Array
Object
。值发生变更时会触发
{name}ValueChanged()
回调。

Actions

动作

The format is
event->controller#method
. Default events exist per element type (click for buttons, input for inputs, submit for forms) so the event can be omitted.
html
{# Explicit event #}
<button data-action="click->hello#greet">Greet</button>

{# Default event (click for button) #}
<button data-action="hello#greet">Greet</button>

{# Multiple actions on same element #}
<input type="text"
       data-action="focus->field#highlight blur->field#normalize input->field#validate">

{# Prevent default #}
<form data-action="submit->form#validate:prevent">

{# Keyboard shortcuts #}
<div data-action="keydown.esc@window->modal#close">
<input data-action="keydown.enter->modal#submit keydown.ctrl+s->modal#save">

{# Global events (window/document) #}
<div data-action="resize@window->sidebar#adjust click@document->sidebar#closeOutside">
格式为
event->controller#method
。不同元素类型有默认事件(按钮对应click,输入框对应input,表单对应submit),因此可以省略事件名。
html
{# Explicit event #}
<button data-action="click->hello#greet">Greet</button>

{# Default event (click for button) #}
<button data-action="hello#greet">Greet</button>

{# Multiple actions on same element #}
<input type="text"
       data-action="focus->field#highlight blur->field#normalize input->field#validate">

{# Prevent default #}
<form data-action="submit->form#validate:prevent">

{# Keyboard shortcuts #}
<div data-action="keydown.esc@window->modal#close">
<input data-action="keydown.enter->modal#submit keydown.ctrl+s->modal#save">

{# Global events (window/document) #}
<div data-action="resize@window->sidebar#adjust click@document->sidebar#closeOutside">

CSS Classes

CSS类

Externalize CSS class names so controllers stay generic:
html
<button data-controller="button"
        data-button-loading-class="opacity-50 cursor-wait"
        data-button-active-class="bg-blue-600"
        data-action="click->button#submit">
    Submit
</button>
javascript
// In controller
this.element.classList.add(...this.loadingClasses);
将CSS类名外置,让控制器保持通用性:
html
<button data-controller="button"
        data-button-loading-class="opacity-50 cursor-wait"
        data-button-active-class="bg-blue-600"
        data-action="click->button#submit">
    Submit
</button>
javascript
// In controller
this.element.classList.add(...this.loadingClasses);

Multiple Controllers

多控制器

An element can have multiple controllers:
html
<div data-controller="dropdown tooltip"
     data-action="mouseenter->tooltip#show mouseleave->tooltip#hide">
    <button data-action="click->dropdown#toggle">Menu</button>
    <ul data-dropdown-target="menu" hidden>...</ul>
</div>
一个元素可以绑定多个控制器:
html
<div data-controller="dropdown tooltip"
     data-action="mouseenter->tooltip#show mouseleave->tooltip#hide">
    <button data-action="click->dropdown#toggle">Menu</button>
    <ul data-dropdown-target="menu" hidden>...</ul>
</div>

Outlets (Cross-Controller Communication)

Outlets(跨控制器通信)

Reference other controller instances by CSS selector:
html
<div data-controller="player"
     data-player-playlist-outlet="#playlist">
    <button data-action="click->player#playNext">Next</button>
</div>

<ul id="playlist" data-controller="playlist">
    <li data-playlist-target="track">Song 1</li>
    <li data-playlist-target="track">Song 2</li>
</ul>
javascript
// In player controller
static outlets = ['playlist'];

playNext() {
    const tracks = this.playlistOutlet.trackTargets;
    // ...
}
通过CSS选择器引用其他控制器实例:
html
<div data-controller="player"
     data-player-playlist-outlet="#playlist">
    <button data-action="click->player#playNext">Next</button>
</div>

<ul id="playlist" data-controller="playlist">
    <li data-playlist-target="track">Song 1</li>
    <li data-playlist-target="track">Song 2</li>
</ul>
javascript
// In player controller
static outlets = ['playlist'];

playNext() {
    const tracks = this.playlistOutlet.trackTargets;
    // ...
}

Lazy Loading (Heavy Dependencies)

懒加载(重依赖场景)

Load controller JS only when the element appears in the viewport. Use for controllers with heavy dependencies (chart libs, editors, maps).
javascript
/* stimulusFetch: 'lazy' */
import { Controller } from '@hotwired/stimulus';
import Chart from 'chart.js';

export default class extends Controller {
    connect() {
        // Chart.js is only loaded when this element enters the viewport
    }
}
The
/* stimulusFetch: 'lazy' */
comment must be the very first line of the file.
仅当元素出现在视口中时才加载控制器JS。适用于依赖较重的控制器(图表库、编辑器、地图)。
javascript
/* stimulusFetch: 'lazy' */
import { Controller } from '@hotwired/stimulus';
import Chart from 'chart.js';

export default class extends Controller {
    connect() {
        // Chart.js is only loaded when this element enters the viewport
    }
}
/* stimulusFetch: 'lazy' */
注释必须放在文件的第一行。

Symfony / Twig Integration

Symfony / Twig 集成

Raw data attributes are the recommended approach -- they work everywhere, are easy to read, and need no special helpers.
twig
{# Raw attributes (preferred) #}
<div data-controller="search"
     data-search-url-value="{{ path('api_search') }}">
Twig helpers exist for complex cases or when generating attributes programmatically:
twig
{# Twig helper #}
<div {{ stimulus_controller('search', { url: path('api_search') }) }}>

{# Chaining multiple controllers #}
<div {{ stimulus_controller('a')|stimulus_controller('b') }}>

{# Target and action helpers #}
<input {{ stimulus_target('search', 'query') }}>
<button {{ stimulus_action('search', 'submit') }}>
推荐直接使用原生的data属性——它们随处可用、可读性强,无需特殊的辅助函数。
twig
{# Raw attributes (preferred) #}
<div data-controller="search"
     data-search-url-value="{{ path('api_search') }}">
复杂场景或需要动态生成属性时可以使用Twig辅助函数:
twig
{# Twig helper #}
<div {{ stimulus_controller('search', { url: path('api_search') }) }}>

{# Chaining multiple controllers #}
<div {{ stimulus_controller('a')|stimulus_controller('b') }}>

{# Target and action helpers #}
<input {{ stimulus_target('search', 'query') }}>
<button {{ stimulus_action('search', 'submit') }}>

Key Principles

核心原则

HTML drives, JS responds. Controllers don't create markup -- they attach behavior to existing HTML. If you find yourself generating DOM in a controller, consider whether a TwigComponent or LiveComponent would be better.
One controller, one concern. A dropdown controller handles dropdowns. A tooltip controller handles tooltips. Compose multiple controllers on the same element rather than building mega-controllers.
Clean up in disconnect(). If
connect()
adds event listeners, timers, or third-party library instances,
disconnect()
must remove them. Turbo navigation will disconnect and reconnect controllers as pages change.
Values over data attributes. Use Stimulus values (typed, with change callbacks) rather than raw
data-*
attributes for data that the controller needs to read or watch.
HTML驱动,JS响应。 控制器不会生成标记——它们只为已有的HTML添加行为。如果你发现自己需要在控制器中生成DOM,可以考虑是否用TwigComponent或LiveComponent更合适。
一个控制器只负责一个功能。 下拉控制器只处理下拉逻辑,提示框控制器只处理提示框逻辑。在同一个元素上组合多个控制器,而不是构建巨型控制器。
在disconnect()中清理资源。 如果
connect()
中添加了事件监听器、定时器或者第三方库实例,
disconnect()
中必须移除它们。Turbo导航在页面切换时会断开并重新连接控制器。
优先使用值而非原始data属性。 对于控制器需要读取或监听的数据,使用Stimulus值(带类型,支持变更回调),而不是原生的
data-*
属性。

References

参考资料

  • Full API (lifecycle, targets, values, actions, classes, outlets): references/api.md
  • Patterns (debounce, fetch, modals, forms, etc.): references/patterns.md
  • Gotchas (common mistakes, debugging, Turbo compatibility): references/gotchas.md
  • 完整API(生命周期、targets、values、actions、classes、outlets):references/api.md
  • 实践模式(防抖、fetch、模态框、表单等):references/patterns.md
  • 注意事项(常见错误、调试、Turbo兼容性):references/gotchas.md