May 22, 2020
•Last updated July 11, 2024
A Tour of Stimulus JS
Today, I'm excited to walk through a great JavaScript framework that has become popular in the Ruby on Rails community called Stimulus.js.
Not another JavaScript framework
Yes, I said framework. Rest assured, it's not as crazy as many you hear about these days. Stimulus.js stems from the Basecamp team. I have a hunch that this framework was introduced to help build their new app called HEY which is due out June 2020.
What is Stimulus.js?
Think of Stimulus as a way to introduce JavaScript to your website or application in a more modular and reusable way. You keep your existing HTML/CSS code and add Stimulus logic where it makes sense. The framework isn't meant to power your entire front-end. React.js and Vue.js for example, have been known to do something like this.
With sprinkles of JavaScript within your website or app code, you can take advantage of the server-side combined with the interactivity of modern JavaScript. To me that's a win-win.
Core concepts
Stimulus.js is consists of three main concepts:
- Controllers
- Actions
- Targets
Through modern JavaScript, Stimulus.js scans your pre-existing markup for controllers
and enables functionality inside. By using data attributes with a convention-driven naming scheme Stimulus.js knows what to look for and how to handle the properties, you author.
A basic example from the documentation looks like this:
The HTML
markup:
<div data-controller="hello">
<input data-target="hello.name" type="text">
<button data-action="click->hello#greet">
Greet
</button>
<span data-target="hello.output">
</span>
</div>
and the accompanying JavaScript
// hello_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
}
Let's break things down:
Controllers
Notice the data-controller="hello"
declaration on a containing div
element. This div
acts as the wrapper around all the controller logic within hello_controller.js
. If the controller data attribute isn't added to the div
, the JavaScript never initializes. You can add multiple controllers to an element if needed.
So you might have markup that looks extended like this:
<div data-controller="hello search">
<!-- Additional markup -->
</div>
The name of the JavaScript file is hello_controller.js
. This is an important convention that Stimulus.js requires.
You give your controller a name, hello
in this case, and append _controller.js
to get things working. The hello
name maps the data-controller="hello"
attribute by design.
A JavaScript file combined with a data-controller="controllerName"
attribute is necessary to initialize any JavaScript code with Stimulus.js.
Targets
Within the context of the data-controller="hello"
div we have another data attribute called data-target="hello.name"
. Think of this as the thing you'd "query" for in traditional JavaScript.
Stimulus.js handles the querying by default with its concept of targets.
Targets are namespaced with dot notation by the parent level controller name. Adding a new target anywhere would need the data-target="hello.myTargetName"
convention enforced. Like controllers, you can have more than one target on an element.
Referencing a target(s) in the JavaScript file happens in a conventional way.
The line below is where you add any targets you've already added to your markup.
// hello_controller.js
export default class extends Controller {
// Defined targets scan the conrtoller HTML for
// data-target="hello.name" or data-target="hello.output"
static targets = [ "name", "output" ]
}
Once defined you can reference them dynamically.
this.outputTarget // Single element (i.e. document.querySelector('.think'))
this.outputTargets // All name targets (i.e. document.querySelectorAll('.thing'))
this.hasOutputTarget // returns true or false whether there is a matching target
You get this functionality for free with Stimulus which is one of my favorite aspects. No longer do you really need to define variables for setup. The naming convention here is strict by design. You'll append the name you gave your target with the word target
or targets
for every new Stimulus.js controller you create.
Actually puting targets to use looks like this:
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
The code above queries for the outputTarget
. Under the hood, it's basically doing the document.querySelector
work. Then, you can code at will with traditional JavaScript. Here we are setting the textContent
of the output target to match what's inside the nameTarget
value input element.
Functions within a Stimulus.js controller are called actions
. Let's talk about those next.
Actions
Think of actions as a way to hook into any JavaScript event on an element. The most common event used is probably a click
event. Looking back at our markup we see another data attribute named data-action="click->hello#greet"
.
There are a number of conventions to unpack here. The first being the click->
text. Here's we're signaling to our Stimulus.js controller that we need to listen for a click event. Following the click->
text is the controller name hello
. This namespaces the logic being applied to the specific controller JavaScript file hello_controller.js
. Finally the #greet
text represents the action itself inside the hello_controller.js
file. Stimulus.js will fire whatever is inside the function called greet
within the hello_controller.js
file only when clicked.
// hello_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
// Our action `greet` is fired as a result of the `data-action="click->hello#greet"` code within the markup
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
}
Combining controllers, targets, and actions get you a fully modular pattern for working with JavaScript. This removes the unnecessary setup and sometimes spaghetti-like code traditional JavaScript is known for.
Additionally, inside any action you can pass the event.
greet(event) {
event.preventDefault()
}
Bonus: Data Maps
Adding additional custom data attributes to your controller code might be necessary as your logic starts to require it. At the parent controller level you can declare new data attributes for use within your controllers.
This might look like the following:
<div data-controller="toggle" data-toggle-open="Toggle open" data-toggle-close="Toggle close">
<button data-target="toggle.button">Toggle open</button>
<div data-target="toggle.toggleable" class="hidden">Some content goes here...</div>
</div>
Inside the controller, you can access these with a handy this.data
object
// controllers/toggle_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["toggleable", "button"]
toggle() {
if (this.toggleableTarget.classList.contains('hidden')) {
this.buttonTarget.textContent = this.data.get('open')
} else {
this.buttonTarget.textContent = this.data.get('close')
}
}
}
On top of this.data.get(key)
you can use this.data.has(key)
, this.data.set(key, value)
, and this.data.delete(key)
,
this.data.get(key)
- Returns the string value of the mapped data attributethis.data.has(key)
- Returns true if the mapped data attribute existsthis.data.set(key, value)
- Sets the string value of the mapped data attributethis.data.delete(key)
- Deletes the mapped data attribute
There's more to unpack
I'll finish off by saying this isn't a comprehensive guide. I think the documentation does a better job than I have here but I wanted to maybe introduce you to something different you might have not considered before. Stimulus.js plays very well with Ruby on Rails apps (especially those that use Turbolinks). I find it a very productive way to write JavaScript even though it is a bit opinionated. Rails are the same way which is why they work so well together. There is also the concept of controllers and actions within a Rails app that rings true in Stimulus.js.
If you would like to learn more about Stimulus.js or see it in use let me know in the comments. I'm happy to put it through the paces to better learn it myself!
Shameless plug
I have a new course called Hello Rails. Hello Rails is a modern course designed to help you start using and understanding Ruby on Rails fast. If you're a novice when it comes to Ruby or Ruby on Rails I invite you to check out the site. The course will be much like these builds but a super more in-depth version with more realistic goals and deliverables. Download your copy today!!
Categories
Collection
Part of the Hotwire and Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.