跳至内容

控制器

控制器是 Stimulus 应用程序的基本组织单元。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
// …
}

控制器是您在应用程序中定义的 JavaScript 类的实例。每个控制器类都继承自 @hotwired/stimulus 模块导出的 Controller 基类。

属性

每个控制器都属于一个 Stimulus Application 实例,并与一个 HTML 元素关联。在控制器类中,您可以访问控制器的

模块

在 JavaScript 模块中定义您的控制器类,每个文件一个。将每个控制器类导出为模块的默认对象,如上例所示。

将这些模块放置在 controllers/ 目录中。将文件命名为 [identifier]_controller.js,其中 [identifier] 对应于每个控制器的标识符。

标识符

标识符是您在 HTML 中用来引用控制器类的名称。

当您向元素添加 data-controller 属性时,Stimulus 会从属性的值中读取标识符,并创建相应控制器类的实例。

例如,此元素有一个控制器,它是 controllers/reference_controller.js 中定义的类的实例

<div data-controller="reference"></div>

以下是如何在 require 上下文中为控制器生成标识符的示例

如果您的控制器文件名为... 其标识符将为...
clipboard_controller.js clipboard
date_picker_controller.js date-picker
users/list_item_controller.js users--list-item
local-time-controller.js local-time

作用域

当 Stimulus 将控制器连接到元素时,该元素及其所有子元素构成控制器的作用域

例如,下面的 <div><h1> 是控制器作用域的一部分,但周围的 <main> 元素不是。

<main>
<div data-controller="reference">
<h1>Reference</h1>
</div>
</main>

嵌套作用域

嵌套时,每个控制器只知道自己的作用域,不包括嵌套在其中的任何控制器的作用域。

例如,下面的 #parent 控制器只知道其作用域中直接的 item 目标,但不知道 #child 控制器的任何目标。

<ul id="parent" data-controller="list">
<li data-list-target="item">One</li>
<li data-list-target="item">Two</li>
<li>
<ul id="child" data-controller="list">
<li data-list-target="item">I am</li>
<li data-list-target="item">a nested list</li>
</ul>
</li>
</ul>

多个控制器

data-controller 属性的值是一个由空格分隔的标识符列表

<div data-controller="clipboard list-item"></div>

页面上任何给定的元素通常有多个控制器。在上面的示例中,<div> 有两个连接的控制器,clipboardlist-item

类似地,页面上的多个元素通常引用同一个控制器类

<ul>
<li data-controller="list-item">One</li>
<li data-controller="list-item">Two</li>
<li data-controller="list-item">Three</li>
</ul>

在此处,每个 <li> 都有自己的 list-item 控制器实例。

命名约定

在控制器类中始终对方法和属性名使用驼峰命名法。

当一个标识符由多个单词组成时,请使用连字符分隔符书写这些单词(即,使用破折号:date-pickerlist-item)。

在文件名中,使用下划线或破折号分隔多个单词(蛇形命名法或连字符分隔符:controllers/date_picker_controller.jscontrollers/list-item-controller.js)。

注册

如果你将 Stimulus for Rails 与导入映射或 Webpack 与 @hotwired/stimulus-webpack-helpers 包一起使用,你的应用程序将自动加载并注册遵循上述约定的控制器类。

如果没有,你的应用程序必须手动加载并注册每个控制器类。

手动注册控制器

要使用标识符手动注册控制器类,首先导入该类,然后在你的应用程序对象上调用 Application#register 方法

import ReferenceController from "./controllers/reference_controller"

application.register("reference", ReferenceController)

你还可以内联注册一个控制器类,而不是从模块中导入它

import { Controller } from "@hotwired/stimulus"

application.register("reference", class extends Controller {
// …
})

根据环境因素阻止注册

如果你只想在满足某些环境因素(例如特定用户代理)时注册和加载控制器,你可以覆盖静态 shouldLoad 方法

class UnloadableController extends ApplicationController {
static get shouldLoad() {
return false
}
}

// This controller will not be loaded
application.register("unloadable", UnloadableController)

当控制器注册时触发行为

如果您想在注册控制器后触发某些行为,您可以添加一个静态的 afterLoad 方法

class SpinnerButton extends Controller {
static legacySelector = ".legacy-spinner-button"

static afterLoad(identifier, application) {
// use the application instance to read the configured 'data-controller' attribute
const { controllerAttribute } = application.schema

// update any legacy buttons with the controller's registered identifier
const updateLegacySpinners = () => {
document.querySelector(this.legacySelector).forEach((element) => {
element.setAttribute(controllerAttribute, identifier)
})
}

// called as soon as registered so DOM may not have loaded yet
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", updateLegacySpinners)
} else {
updateLegacySpinners()
}
}
}

// This controller will update any legacy spinner buttons to use the controller
application.register("spinner-button", SpinnerButton)

afterLoad 方法将在控制器注册后立即被调用,即使 DOM 中不存在受控元素。该函数将绑定到原始控制器构造函数以及两个参数一起被调用;注册控制器时使用的 identifier 和 Stimulus 应用程序实例。

使用事件进行跨控制器协调

如果您需要控制器相互通信,您应该使用事件。Controller 类有一个名为 dispatch 的便捷方法,它使此操作变得更加容易。它将 eventName 作为第一个参数,然后自动加上控制器名称并用冒号分隔。有效负载保存在 detail 中。它的工作原理如下

class ClipboardController extends Controller {
static targets = [ "source" ]

copy() {
this.dispatch("copy", { detail: { content: this.sourceTarget.value } })
navigator.clipboard.writeText(this.sourceTarget.value)
}
}

然后可以将此事件路由到另一个控制器上的操作

<div data-controller="clipboard effects" data-action="clipboard:copy->effects#flash">
PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>
<button data-action="clipboard#copy">Copy to Clipboard</button>
</div>

因此,当调用 Clipboard#copy 操作时,Effects#flash 操作也将被调用

class EffectsController extends Controller {
flash({ detail: { content } }) {
console.log(content) // 1234
}
}

如果这两个控制器不属于同一个 HTML 元素,则需要将 data-action 属性添加到接收控制器的元素中。如果接收控制器的元素不是发送控制器的元素的父元素(或相同元素),则需要将 @window 添加到事件中

<div data-action="clipboard:copy@window->effects#flash">

dispatch 接受附加选项作为第二个参数,如下所示

选项 默认值 说明
detail {} 空对象 参见 CustomEvent.detail
target this.element 参见 Event.target
prefix this.identifier 如果前缀为假值(例如 nullfalse),则只使用 eventName。如果您提供一个字符串值,则 eventName 将被添加在提供的字符串和冒号之前。
bubbles true 参见 Event.bubbles
cancelable true 参见 Event.cancelable

dispatch 将返回生成的 CustomEvent,您可以使用它来提供一种方法,以便任何其他侦听器都可以取消事件,如下所示

class ClipboardController extends Controller {
static targets = [ "source" ]

copy() {
const event = this.dispatch("copy", { cancelable: true })
if (event.defaultPrevented) return
navigator.clipboard.writeText(this.sourceTarget.value)
}
}
class EffectsController extends Controller {
flash(event) {
// this will prevent the default behaviour as determined by the dispatched event
event.preventDefault()
}
}

直接调用其他控制器

如果由于某种原因无法使用事件在控制器之间进行通信,您可以通过应用程序中的 getControllerForElementAndIdentifier 方法访问控制器实例。只有在您遇到无法通过更通用的使用事件的方式解决的独特问题时才应使用此方法,但如果您必须使用,方法如下

class MyController extends Controller {
static targets = [ "other" ]

copy() {
const otherController = this.application.getControllerForElementAndIdentifier(this.otherTarget, 'other')
otherController.otherMethod()
}
}

下一步:生命周期回调