21 January 2021
A Quick Look at Stimulus.js
Stimulus.js is a JavaScript framework often associated with Hotwire: a new addition to the Ruby on Rails framework. In preparation for a future article on Hotwire, I started with a “deep dive” on Stimulus. In this article, I will take a look at version 2.0.0 which is the latest version as of this writing. I started with the online handbook, which does an excellent job of explaining things by guiding you through creating a simple project. While JavaScript frameworks are plentiful, this one takes a different approach. It uses an “HTML-centric approach to state and wiring”. So let’s take a look at what this is and get a feel for using Stimulus using the code from the handbook.
Stimulus works by linking JavaScript objects to HTML elements by matching them up using naming conventions and DOM element attribute specifications. Stimulus has 4 main JavaScript objects: controllers, actions, targets, and values. Let’s look at these in some detail, using examples from the handbook.
Let’s start by looking at the controller. Its main purpose is to contain the other objects that will be active for a specific section of HTML. While this is similar to MVC controllers of other languages, this controller differs in being strongly coupled to the DOM, limiting itself to one or more container elements within one or more HTML pages rendered in the browser. This is easier to see in an example than trying to describe.
// in file controllers/example_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "It works!"
}
}
The controller gets its name from the filename, example_controller.js. The convention is [controller name]_controller, making this the example controller. This example has one method, connect(), which is a lifecycle callback that is invoked when the controller is matched and connected to something in the DOM.
<h1 data-controller="example"></h1>
The above code is from the HTML and is the DOM element that the controller is connected to. The connection is made because this element has the attribute of data-controller with a value of example. As all controllers are loaded when the app is launched in the browser, the connection happens when an element in the DOM has a matching attribute. Once the connection is made, the connect() method fires and the text “It works!” is injected into the H1 tag and displayed on the screen.
Next, let’s look at actions. They associate HTML elements with controller methods and as such can be thought of as DOM event listeners.
<div data-controller="slideshow" data-slideshow-index-value="1">
<button data-action="slideshow#previous"> ← </button>
<button data-action="slideshow#next"> → </button>
</div>
In the HTML above, there are 2 buttons for scrolling through a slideshow. Each has a DOM event listener for the default click event that is mapped to a method, previous() or next(), in the ‘slideshow’ controller.
Our next object is the target. A target is a reference to a DOM element from within the controller. This might be used to get text from an input field or to output text to an HTML element, as we saw with the controller example above. Again, let’s look at an example.
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="hello#greet">Greet</button>
</div>
In the HTML above, we are connecting with a controller named hello. We also have a target in that controller named name, and a click event listener with a related action, hello#greet, set up on the button.
// in file controllers/hello_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "name" ]
greet() {
console.log(`Hello ${this.name}!`)
}
get name() {
return this.nameTarget.value
}
}
In the controller code above, the targets are in a static array named targets that contains the value of the attribute(s), data-hello-target, we are mapping to in the DOM. In this example that is the name value that matches our DOM input element. We see that the greet() method uses a getter method, get name(), to retrieve the value of our connected input field and then logs that value to the console.
Finally, let’s look at values. Values are used to store state in Stimulus. They exist in the DOM as attributes. Let’s look again at our slideshow code to see this.
// in file controllers/slideshow_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "slide" ]
static values = { index: Number }
next() {
this.indexValue++
}
previous() {
this.indexValue--
}
indexValueChanged() {
this.showCurrentSlide()
}
showCurrentSlide() {
this.slideTargets.forEach((element, index) => {
if (this.indexValue < 0) {
this.indexValue = 3
}
this.indexValue = this.indexValue % 4
element.hidden = (index != this.indexValue)
})
}
}
Values are referenced as a static object named values inside the controller. The values object contains key/value pairs where the key is named so as to connect to the DOM and the value is the data type. JavaScript data types include Number, String, Object, Boolean, and Array. In our example above, we have a value with the key of index that is a data type of Number. We can read and set this using the property this.indexValue. As I mentioned, the actual values are not stored within the controller or in some kind of JavaScript state, but reside in the DOM. Let’s look at that now.
<div data-controller="slideshow" data-slideshow-index-value="1">
<button data-action="slideshow#previous"> ← </button>
<button data-action="slideshow#next"> → </button>
<div data-slideshow-target="slide">🐵</div>
<div data-slideshow-target="slide">🙈</div>
<div data-slideshow-target="slide">🙉</div>
<div data-slideshow-target="slide">🙊</div>
</div>
This is the HTML that goes with the slider controller. We know that because it has the attribute data-controller=”slideshow”. It has another attribute as well, data-slideshow-index-value=”1”. This is where the actual value of our index value is stored. As that value is changed by clicking the next and previous buttons you can see the value of the attribute change while viewing the HTML in the DOM inspector of your browser.
Summary
Stimulus.js is an easy to use Javascript framework. In this article, we saw that it uses naming conventions to tie JavaScript to DOM elements, abstracting out the complexities of wiring these together. If you are a Ruby on Rails developer, then this approach of convention over configuration is what you are used to. In using Stimulus, you can be productive from the start, unlike complex frameworks like React and Angular. Even with this simplicity, the framework has the depth to build complex user experiences. It even works with APIs that return JSON. You just need to transform the JSON into HTML before injecting it into the DOM. I hope you will consider trying Stimulus.js in a future project, and I recommend taking a look at Stimulus.js before jumping into Hotwire, as I have done.