Stimulus Features You (Didn't) Know

railsdesigner

Rails Designer

Posted on November 28, 2024

Stimulus Features You (Didn't) Know

This article was originally published on Rails Designer


🤩 Black Friday Alert! 📣 Rails Designer is now available with a discount up to 50% 🤑


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. In this article I want to explore those feature, I think you might not 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 return true or false).

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:

  • update a counter for the number of search results (coming via turbo stream request);
  • reorder (by alphabet) a list of people when they are dynamically added (assuming a turbo stream append/prepend);
  • hide or show loading states based on a target connected (via a turbo stream).

This 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})`;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Then the update method will look like this:

update({ params: { value } }) {
  this.#setClass(value);
}
Enter fullscreen mode Exit fullscreen mode

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 actions 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"
>
Enter fullscreen mode Exit fullscreen mode
export default class extends Controller {
  validate(event) {
    if (/[0-9]/.test(event.key)) {
      event.target.value += event.key
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

This might be something you stumbled upon by accident (or frustration?), but it is 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
  }
}
Enter fullscreen mode Exit fullscreen mode

This controller won't be registered and loaded at all. 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 let me know below.

💖 💪 🙅 🚩
railsdesigner
Rails Designer

Posted on November 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related