Adding Our First Controller
Explore how to add your first Stimulus controller to manage interactive elements like toggling content visibility. Understand attaching controllers to DOM elements, defining the connect method in TypeScript, and how Stimulus handles multiple controllers and dynamic DOM updates. This lesson prepares you to enhance front-end interactivity in Rails applications.
We'll cover the following...
How controllers work
The first thing we’re going to do with Stimulus is, create a “Show/Hide” button that will toggle the contents of the favorite concerts block on our page.
The bulk of our Stimulus code will go into a controller. Similar to our Rails controller, a Stimulus controller is where we store responses to events. But, Stimulus controllers are different in that they have less structure and fewer expectations than a Rails controller.
To invoke a controller, we add an attribute named data-controller to a DOM element on our page. The value of the attribute is the name of the controller. If we add an attribute data-controller="toggle", then Stimulus attaches it to a ToggleController, which it expects will be at controllers/toggle_controller.ts (or .js).
If the controller’s name is more than one word, we use dash case, so it would be fancy-color, not fancyColor. The controller has the same scope as the DOM element it’s attached to. This means that any events we want dispatched to the controller need to happen inside the controller’s DOM element. Any elements we designate as targets for the controller also need to be inside the controller’s DOM element.
Creating a controller for show/hide
In our app we want to have a “Show/Hide” button, which should include the ability to press the button, hide the list of favorite concerts, and change the text of the button. This means our Stimulus controller needs to be attached to a DOM element that encompasses both the button and all the favorite concerts.
We have such a DOM element: the parent <section> element in our favorites/_list partial. Let’s give it a data-controller attribute:
Here we add the DOM attribute data-controller and give it the value favorite-toggle. By itself this doesn’t do much. When the page is loaded, however, Stimulus will look for a matching controller. Using convention over configuration, that controller should be in the file app/javascript/controllers/favorite_toggle_controller.ts.
Here’s a basic controller that doesn’t do anything yet:
The first line uses ES6 module syntax to import the Controller class from the stimulus module. We’ll talk about this more in Introduction to webpack, but for now, just know that using webpack allows this import statement to be reconciled against the Stimulus module living in our node_modules directory.
Next we declare our class, again using ES6 module keywords. The export keyword means that what is about to be defined is publicly visible to other files that might import this file. The default keyword means that the thing about to be defined is the default item exported if the importing module does not specify.
Inside the class, we temporarily define one method: connect(): void. The connect() method is automatically called by Stimulus when a controller is instantiated and connected to a DOM element. It is here only to allow us to see that the controllers are instantiated when the page is reloaded.
The :void is our first part of TypeScript-specific syntax and merely tells TypeScript that the connect method doesn’t return a value. TypeScript can figure that out for itself, but errors are prevented if we get in the habit of specifying whether we expect a return value in the function declaration.
Now reload the page and look at the browser console. We see that the message has now been printed to the console.
The Stimulus library searches the DOM for data-controller attributes and every time it finds one, it attempts to find a matching controller for it using the name of the controller to find the matching file. This is true no matter when the DOM is changed. Stimulus connects on initial page load and also recognizes DOM changes once the page has loaded, no matter what the source of the change is.
At this point, it’s worth noting a couple of points about Stimulus code structure that are important to keep in the back of our minds.
First, we can declare the same controller any number of times in a single document. We can use the same name over and over again to get separate instances of the same controller. We can even have the same controller nested inside itself:
This works just fine. Anything declared as part of a thing controller is connected to its nearest matching ancestor in the DOM tree.
Secondly, a single element can be attached to multiple controllers, separated by spaces:
This element instantiates three different controllers: ColorController, SizeController, and ShapeController (assuming the controllers have actually been defined in app/packs/controllers). Inside the element, targets and actions can be directed at any of the controllers.
These two features make Stimulus great for small, generic, composable controllers. Controllers are great, but they don’t do much on their own. They need to be attached to actions.
The “Show/Hide" button is not doing anything yet, we’ll make that work in the next chapters.
Here’s the application we have so far:
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
}