If you boil it all down, here's the basic flow through the app (with some gotcha's)
When there is a request for a page in a Rails-only app, the controller handles and routes the request, and renders a View. When the DOM is being written, some special Stimulus tags cue the instantiation of an associated Stimulus controller. The DOM also listens for user actions that are attached to some page elements via some other special Stimulus tags. When an event occurs, the associated function is called and runs its code to update the DOM without reloading the page.
So... what are the special tags? Here's a longer breakdown:
- A section of html is wrapped in a div with a
data-controller
tag. (The section that is wrapped should contain all the page elements that you plan to listen to or act upon using that controller). The value corresponds to the name of a StimulusJS controller:
<div data-controller="dropdown">
- file
app/javascripts/controllers/dropdown_controller.js
with a controller class namedDropdownController
-
Any page element that will be 'listened to' or 'affected by' the controller needs to be 'registered' (my wording) with the controller. This is done by adding a data tag & target attribute to each. Then a corresponding string is added to a special
targets
array in the Stimulus controller: HTML data tags & attributes-
for non-Rails elements:
data-controllername-target="targetName"
-
for Rails forms/elements:
data: { 'controllername-target': 'targetName' }
-
Note that there are a few ways that the data target tag can be set up. But when you use
data-target='controllername.targetName'
, you get a notice in the console that this is a deprecated set up and that Stimulus prefers that you specify the controller in the data tag instead of the dot notation in the value:Please replace data-target="dropdown.sessionsIds" with data-dropdown-target="sessionsIds". The data-target attribute is deprecated and will be removed in a future version of Stimulus.
Stimulus controller targets array
var targets = ["targetName']
, e.g.var targets = ["sessionsIds", "originIds", 'commIds']
Once the Target elements are registered, their properties and attributes are available to the controller in various ways (after you have created some controller functions and 'registered' them to some page elements). You can access and manipulate them using Javascript API methods:
this.targetNameTarget
will let you do things likethis.sessionsIdsTarget.selectedOptions
orthis.sessionsIdsTarget.selectedOptions[0].value
orthis.commIdsTarget.appendChild(someChildVar)
The data-targets do not stimulate any events or functions. They aren't like 'listeners', they are more like finding an element using $('#some-id) in a console to access the attrs and props.
-
-
Elements that have a data-target may or may not need an associated listener and action. For elements that do need to stimlulate an event or action, add a data-action tag. There are specific StimulusJS 'listeners' (or "DOM events to listen for") for different elements (https://stimulus.hotwire.dev/reference/actions#event-shorthand).
A data-action tag can look like this:
"listener->controller#functionInMyController"
, e.g."click->dropdown#updateCommIdList"
... and the corresponding function would just look like this:
updateCommIdList() { // do stuff }
At this point you can write javascript and use the javascript API for working with DOM elements in order to make things happen (like updating a dropdown, showing/hiding elements, adding new classes to change the css)
It's important to note a few gotcha's at this point.
-
In the controller,
this
is the object of the controller itself. Even if you are working with some inner html, or have accessed an element in some way,this
will always be the controller object. That's why you callthis.targetNameTarget
when you want to access the page element associated with the target name. -
If your function isn't simple enough to span a few lines and you want to break out some code into other functions, you'll need to think of them like Ruby Class Methods. The function that you registered to a DOM element will be called when the listening-event is stimulated. But that function can only call other functions by calling
this.otherFunction
.Ex:
import { Controller } from "stimulus" export default class FancyThingsController extends Controller { static targets = ["thing1", "thing2"] makeItFancy() { var foo = true var bar = true this.otherFunction(foo, bar) } otherFunction(foo, bar) { foo = !bar return foo }
-
Once you have a function and associated data-action tag in your page, you have all the fundamental pieces in place.
When the DOM is being written, the data-controller
tags cue the instantiation of the associated Stimulus controller, and the DOM listens for the user actions that are attached to some page elements via data-target
tags and data-action
tags. When a data-action's event occurs, the data-action's function is called and runs its code to update the DOM without reloading the page. The function can access and manipulate attrs and props of any page elements that have a data-target tag registered to that controller.
Could all stimulus controllers just be loaded at the top of the body by putting all the data-controller tags in one place?
- I suppose yes, but if you have a lot, you'd be instantiating a ton of unused javascript classes for each new request and that might slow page loads. Probably better to just place the data controller tags at the smallest scope possible in a view or partial.
- So maybe you could have a single div for all controllers at the top of the parent page being loaded... so if your data-targets and data-actions are in a partial on an Index View, and you know all the partials will be loaded and all the Stimulus Controllers will be instantiated, you could throw the data-controller tags all in one div at the top of the Index View (and be sure to have a closing div in the view too)