Skip to content

Controllers

A controller is the basic organizational unit of a Stimulus application.

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
// …
}

Controllers are instances of JavaScript classes that you define in your application. Each controller class inherits from the Controller base class exported by the @hotwired/stimulus module.

Properties

Every controller belongs to a Stimulus Application instance and is associated with an HTML element. Within a controller class, you can access the controller’s:

Modules

Define your controller classes in JavaScript modules, one per file. Export each controller class as the module’s default object, as in the example above.

Place these modules in the controllers/ directory. Name the files [identifier]_controller.js, where [identifier] corresponds to each controller’s identifier.

Identifiers

An identifier is the name you use to reference a controller class in HTML.

When you add a data-controller attribute to an element, Stimulus reads the identifier from the attribute’s value and creates a new instance of the corresponding controller class.

For example, this element has a controller which is an instance of the class defined in controllers/reference_controller.js:

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

The following is an example of how Stimulus will generate identifiers for controllers in its require context:

If your controller file is named… its identifier will be…
clipboard_controller.js clipboard
date_picker_controller.js date-picker
users/list_item_controller.js users--list-item
local-time-controller.js local-time

Scopes

When Stimulus connects a controller to an element, that element and all of its children make up the controller’s scope.

For example, the <div> and <h1> below are part of the controller’s scope, but the surrounding <main> element is not.

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

Nested Scopes

When nested, each controller is only aware of its own scope excluding the scope of any controllers nested within.

For example, the #parent controller below is only aware of the item targets directly within its scope, but not any targets of the #child controller.

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

Multiple Controllers

The data-controller attribute’s value is a space-separated list of identifiers:

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

It’s common for any given element on the page to have many controllers. In the example above, the <div> has two connected controllers, clipboard and list-item.

Similarly, it’s common for multiple elements on the page to reference the same controller class:

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

Here, each <li> has its own instance of the list-item controller.

Naming Conventions

Always use camelCase for method and property names in a controller class.

When an identifier is composed of more than one word, write the words in kebab-case (i.e., by using dashes: date-picker, list-item).

In filenames, separate multiple words using either underscores or dashes (snake_case or kebab-case: controllers/date_picker_controller.js, controllers/list-item-controller.js).

Registration

If you use Stimulus for Rails with an import map or Webpack together with the @hotwired/stimulus-webpack-helpers package, your application will automatically load and register controller classes following the conventions above.

If not, your application must manually load and register each controller class.

Registering Controllers Manually

To manually register a controller class with an identifier, first import the class, then call the Application#register method on your application object:

import ReferenceController from "./controllers/reference_controller"

application.register("reference", ReferenceController)

You can also register a controller class inline instead of importing it from a module:

import { Controller } from "@hotwired/stimulus"

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

Preventing Registration Based On Environmental Factors

If you only want a controller registered and loaded if certain environmental factors are met – such a given user agent – you can overwrite the static shouldLoad method:

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

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

Trigger Behaviour When A Controller Is Registered

If you want to trigger some behaviour once a controller has been registered you can add a static afterLoad method:

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)

The afterLoad method will get called as soon as the controller has been registered, even if no controlled elements exist in the DOM. The function will be called bound to the original controller constructor along with two arguments; the identifier that was used when registering the controller and the Stimulus application instance.

Cross-Controller Coordination With Events

If you need controllers to communicate with each other, you should use events. The Controller class has a convenience method called dispatch that makes this easier. It takes an eventName as the first argument, which is then automatically prefixed with the name of the controller separated by a colon. The payload is held in detail. It works like this:

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

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

And this event can then be routed to an action on another controller:

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

So when the Clipboard#copy action is invoked, the Effects#flash action will be too:

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

If the two controllers don’t belong to the same HTML element, the data-action attribute needs to be added to the receiving controller’s element. And if the receiving controller’s element is not a parent (or same) of the emitting controller’s element, you need to add @window to the event:

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

dispatch accepts additional options as the second parameter as follows:

option default notes
detail {} empty object See CustomEvent.detail
target this.element See Event.target
prefix this.identifier If the prefix is falsey (e.g. null or false), only the eventName will be used. If you provide a string value the eventName will be prepended with the provided string and a colon.
bubbles true See Event.bubbles
cancelable true See Event.cancelable

dispatch will return the generated CustomEvent, you can use this to provide a way for the event to be cancelled by any other listeners as follows:

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()
}
}

Directly Invoking Other Controllers

If for some reason it is not possible to use events to communicate between controllers, you can reach a controller instance via the getControllerForElementAndIdentifier method from the application. This should only be used if you have a unique problem that cannot be solved through the more general way of using events, but if you must, this is how:

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

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

Next: Lifecycle Callbacks