May 22, 2020

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 attribute
  • this.data.has(key) - Returns true if the mapped data attribute exists
  • this.data.set(key, value) - Sets the string value of the mapped data attribute
  • this.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!!

Leave a reply

Sign in or Sign up to leave a response.

1 response

BS
Icons/flag
Icons/link diagonal

Hi,

Thanks for the video. I really like the one with Stripe where you used Stimulus for the Perks.

I would really love to see a little bit more of Stimulus and what is the best way to use it. For example, creating a shopping cart where you can update the quantity of items or remove them. There you will also have live price calculation for each item and the subtotal of the cart changing as well.

Since each update or remove is an API call, it will be interesting to see how to do that with Rails. Is it using Action Cable as you said or we can use simple API calls to the BE and update the FE when successful.

Looking forward to it! Thanks again for the great content!

Est. reading time: 7 minutes
Stats: 400 views

Categories

Collection

Part of the Ruby on Rails collection