Stimulus Features You (Didn't) Know

3d rendering from hilly area, with lots of diverging paths

Stimulus is advertised as a modest framework for the HTML you already have. It still packs quite a few features that you (didn’t) know (or have forgotten about).

Existential properties

Every API in stimulus (targets, classes, values and outlets) has the existential attribute option. Meaning you can check if an attribute is available.

  • hasButtonTarget;
  • hasButtonClass;
  • hasLabelValue;
  • hasActionsOutlet.

You can do your logic based off of that boolean value (it returns true or false).

update() {
  if (!this.hasButtonTarget) return;

  this.buttonTarget.classList.add(this.hasButtonClass ? this.buttonClass : "btn");
}

Connected and disconnected callbacks for targets

You most likely know about the connect and disconnect lifecycle methods for Stimulus classes. But there are also [name]TargetConnected() and [name]TargetDisconnected() method. These are called when a target becomes connected or disconnected.

This has many use cases, when using with, for example, Turbo Streams as responses:

  • update a counter for the number of search results;
  • reorder (by alphabet) a list of people when they are dynamically added;
  • hide or show loading states based on a target connected.

This is how it is used:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["item", "count"];

  // …

  itemTargetConnected() {
    this.#updateItemCount();
  }

  itemTargetDisconnected() {
    this.#updateItemCount();
  }

  // private

  #updateItemCount() {
    this.countTarget.textContent = `(${this.itemTargets.length})`;
  }
}

Note that the [name]TargetDisconnected() method gets fired before the disconnect() lifecycle method.

I wrote an article on Connected and Disconnected Target Callbacks with Stimulus if you want to learn more.

Passing params to actions

Sometimes you need to pass some attribute to a certain method. That’s easy and works like this:

<div data-controller="theme">
  <button data-action="theme#update" data-theme-value-param="dark">
    Lights Off
  </button>
</div>

Then the update method will look like this:

update({ params: { value } }) {
  this.#setClass(value);
}

It follows this structure data-[identifier]-[param-name]-param. It can take any type for a value from a String, Number to an Object and a Boolean. Refer to the docs for more.

Prevent default

Talking about actions. There are various action-options available. :prevent is one of them. You probably have used event.preventDefault() in your methods. Stimulus provides a shortcut for it. Let’s take this example:

<input
  type="text"
  data-controller="input"
  data-action="keypress->input#validate:prevent"
  placeholder="Numbers only"
>
export default class extends Controller {
  validate(event) {
    if (/[0-9]/.test(event.key)) {
      event.target.value += event.key
    }
  }
}

Without :prevent (preventDefault()), invalid characters (non-numbers) would still appear in the input even after the validation check.

Stop propagation

There is also :stop. This is a shortcut for event.stopPropagation() within the controller’s action/method. This prevents the event from bubbling up through the DOM. An example is probably easier:

<div data-controller="dropdown" data-action="click@window->dropdown#hide">
  <button data-action="dropdown#toggle:stop">
    Toggle Dropdown
  </button>

  <div data-dropdown-target="menu">
    <a href="/profile">Profile</a>
    <a href="/settings">Settings</a>
    <a href="/logout">Logout</a>
  </div>
</div>

Without :stop the click event would bubble up the DOM and reach the hide action defined in the parent element’s data-action. No good!

👀 Events bubbling up? Prevent Default? Stop propagation? Do you sorta know what it does, but not exactly? Pre-order JavaScript for Rails Developers.

Self

Another custom action option is self. It will only fire when on the element itself, not its children.

  <div data-controller="dropdown">
    <div data-action="click->dropdown#toggle:self">
      Menu Header

      <ul class="menu-items">
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
    </div>
  </div>

In above example toggle will only be fired when Menu Header is clicked, not when .menu-items (or any of its children) is clicked.

Check out this article if you are interested in creating your own custom action options? Or see stimulus-fx a collection of custom action options I built. ☺️

Plural CSS classes

If you want to add/remove or toggle a CSS class. The classes API is great for that. Stimulus allows you to add multiple CSS classes at once, you just need to know the convention and a bit of JavaScript. It works like this:

export default class extends Controller {
  static classes = ["scrolling"];

  scroll() {
    this.element.classList.add(...this.scrollingClasses);
  }
}

I wrote an article that goes into details about toggling multiple CSS classes with Stimulus.

Nested Scopes

Each controller in Stimulus operates within its own isolated scope. This means it can only interact with targets defined in its immediate context. Parent controllers cannot access targets from nested child controllers, and vice versa.

Wanna see code? I heard you like tabs in your tabs:

<div data-controller="tabs">
  <div data-tabs-target="panel">
    <!-- This panel is found by the parent tabs controller -->
  </div>

  <div data-controller="tabs">
    <div data-tabs-target="panel">
      <!-- This panel is only found by the nested tabs controller -->
    </div>
  </div>
</div>

This might be something you stumbled upon by accident (or frustration?), but it still a good feature to know about.

shouldLoad method

Do you want certain features to only work on mobile devices, or maybe you have some special functionality just for touch devices? You can conditionally load a controller with shouldLoad. Simply like this:

export default class extends Controller {
  static get shouldLoad() {
    return window.innerWidth <= 768 && "ontouchstart" in window
  }
}

This controller won’t be registered and loaded at all unless the above conditions match. So there won’t be any creation of the controller instance. This is different from, say adding return false to your connect lifecycle method, where an instance is created.

afterLoad method

Sometimes you need to run some setup code right after a controller is registered, regardless of whether any elements are using it yet (connected). afterLoad is perfect for this.

Again, a code snippet might help:

export default class extends Controller {
  static afterLoad(identifier, application) {
    // anything you need to get done here
  }
}

Honestly, I have thought about afterLoad() for a fair bit and haven’t come up with a real use-case for it. I’ve simply included as it’s semi-related to shouldLoad() and it’s good to know it’s there. So if you find a use-case for it, please contact me.

That’s all the features of Stimulus you (didn’t) know.

What is your score out of these 10?

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built using ViewComponent

  • Designed with Tailwind CSS

  • Enhanced with Hotwire