Search⌘ K
AI Features

Adding Our First Controller

Explore how to add your first Stimulus controller in a Rails app to respond to user events. Understand how controllers attach to DOM elements, manage show/hide actions, and simplify event handling using data attributes.

How controllers work

The bulk of your Stimulus code will go through a controller, which is similar to your Rails controller in that it’s where the responses to events are stored, but there’s much less structure and expectations around a Stimulus controller than a Rails controller.

To invoke a controller, you add an attribute named data-controller to a DOM element on your page. The value of the attribute is the name of the controller. If the controller’s name is more than one word, you 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 you want dispatched to the controller need to happen inside the controller’s DOM element. Any elements that you 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 make a working version of the show/hide button for each day, so we want to press the button, hide the concerts for that day, and change the text of the button. This means our controller needs to be attached to a DOM element that encompasses both the button and all the concerts for that day.

Happily, we have such a DOM element. It’s the one in app/views/schedules/show.html.erb inside the loop that we’ve already given the DOM class of day-body to. We want to make it look like this:

HTML
<section
class="day-body"
id="day-body-<%= schedule_day.day.by_example("2006-01-02")%>"
data-controller="day-toggle">

We are adding the DOM attribute data-controller and giving it the value day-toggle. By itself this doesn’t do much. But when the page is loaded, Stimulus will look for a matching controller. Using convention over configuration, that controller should be in the file app/javascript/controller/day_toggle_controller.ts.

Here’s a basic controller that doesn’t do anything yet:

TypeScript 3.3.4
import { Controller } from "stimulus"
export default class DayToggleController extends Controller {
connect() {
console.log("The controller is connected")
}
}

The first line uses ES6 module syntax to import the Controller class from the stimulus module. We’ll talk about this more in the Webpack chapter, 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 the thing about to be defined is publicly visible by other files that might import this file, and the default keyword means the thing about to be defined is the default item exported if the importing module does not specify what it wants.

Inside the class, we define one method: connect(). Don’t get too attached to it, because we’re probably not going to keep it. The connect() method is automatically called by Stimulus when a controller is instantiated. I included it here only to allow us to see that the controllers are, in fact, instantiated when the page is reloaded.

Okay, reload the page and look at the browser console. You’ll see that the message has been printed to the console six times. Yep, six times.

Our original version of this code in the last chapter required us to find all the DOM elements we wanted to use and loop over them to attach event listeners to them. In Stimulus, that happens automatically. 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.

Since the DOM element that data-controller is connected to is defined inside an each loop, every time through the list, Stimulus instantiates a new controller instance. That’s quite cool and simplifies the work we need to do to connect the DOM element to the eventual code we want executed.

At this point it’s worth making a couple of points that aren’t important yet, but worth keeping in the back of your head because of the way they will inform how we structure Stimulus code going forward.

You can declare the same controller an arbitrary amount of times in a single document. We’ve already seen that in our loop, but the loop part doesn’t matter, you can use the same name over and over again to get separate instances of the same controller. You can even have the same controller nested inside itself:

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

This works just fine—anything declared as part of a thing controller is connected to it’s nearest matching ancestor in the DOM tree.

It’s important to note that a single element can be attached to multiple controllers, separated by spaces:

<div data-controller="color size shape">
</div>

This element instantiates three different controllers: ColorController, SizeController, and ShapeController (assuming the controllers have actually been defined in app/javascript). Inside the element, targets and actions can be directed at any of the controllers.

Controllers are great, but they don’t do much on their own; they need to be attached to actions.

Here’s the application we have so far:

Bud16ocblob�� @� @� @� @6appIlocblob����������������applg1Scomp�appmoDDblob�AappmodDblob�Aappph1Scomp`babel.config.jsIlocblob����������������binIlocblob����������������binlg1Scomp#binmoDDblob�AbinmodDblob�Abinph1Scomp�configIlocblob����������������configlg1Scomp��configmoDDblob��AconfigmodDblob��Aconfigph1Scomp`	config.ruIlocblob����������������dbIlocblob����������������dblg1Scomp6�dbmoDDblob��AdbmodDblob��Adbph1Scomp�GemfileIlocblob����������������Gemfile.lockIlocblob����������������libIlocblob����������������liblg1Scomp�libmoDDblob�AlibmodDblob�Alibph1Scomppackage.jsonIlocblob����������������postcss.config.jsIlocblob����������������publicIlocblob����������������publiclg1ScomppublicmoDDblob��ApublicmodDblob��Apublicph1Scomp@RakefileIlocblob����������������specIlocblob����������������speclg1Scomp)�specmoDDblob��AspecmodDblob��Aspecph1Scomp`storageIlocblob����������������storagelg1ScompstoragemoDDblob�AstoragemodDblob�Astorageph1Scomp
tsconfig.jsonIlocblob����������������vendorIlocblob����������������vendorlg1ScompvendormoDDblob�AvendormodDblob�Avendorph1Scomp	yarn.lockIlocblob����������������EDSDB `� @� @� @licph1Scomp@RakefileIlocblob����������������specIlocblob����������������speclg1Scomp)�specmoDDblob��AspecmodDblob��Aspecph1Scomp`storageIlocblob����������������storagelg1ScompstoragemoDDblob�AstoragemodDblob�Astorageph1Scomp
tsconfig.jsonIlocblob����������������vendorIlocblob����������������vendorlg1ScompvendormoDDblob�AvendormodDblob�Avendorph1Scomp	yarn.lockIlocblob����������������