DEV Community

Andy Leverenz
Andy Leverenz

Posted on • Originally published at web-crunch.com on

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>

Enter fullscreen mode Exit fullscreen mode

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}!`
  }
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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"] 

}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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}!`
  }

Enter fullscreen mode Exit fullscreen mode

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}!`
  }
}

Enter fullscreen mode Exit fullscreen mode

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()
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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')
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)