The best one-line Stimulus power move

simone-biles.jpg

Stimulus is a tiny and absurdly productive JavaScript framework for developers who are looking for just the right amount of structure (lifecycle events and standard HTML) without attempting to re-invent how the web works (no template rendering or routing). It is criminally underappreciated in the JavaScript community.

When using Stimulus, you write controllers in JavaScript and attach instances of those controllers to DOM elements by setting data-controller="controller-name".

Unfortunately, there’s no easy way to access methods in a controller from another controller, external scripts, jQuery plugins or the console… or is there?


Before I do the big reveal, there is technically a way to access another controller instance from inside of a controller. It’s an undocumented method so there’s no guarantee that it won’t disapper someday, but the real clue that this isn’t intended to be used is the laughably long name: this.application.getControllerForElementAndIdentifier(element, controller).

Controllers have access to the global Stimulus application scope, which has getControllerForElementAndIdentifier as a member function. If you have a reference to the element with the controller attached and the name of the controller, you can get a reference to any controller on your page. Still, this doesn’t offer any solutions to developers working outside of a Stimulus controller.


Here’s what we should all do instead.

In your controller’s connect() method, add this line:

this.element[this.identifier] = this

Boom! This hangs a reference to the Stimulus controller instance off the DOM element that has the same name as the controller itself. Now, if you can get a reference to the element, you can access element.controllerName anywhere you need it.

What’s cool about this trick is that since Stimulus calls connect() every time an instance is created, you can be confident that your elements will always have a direct reference to their parent, even if they are attached to elements that are dynamically inserted by something like morphdom.

this.identifier can be replaced with any camelCase string as you desire.


I’ll provide a basic example.

// test_controller.js
import { Controller } from 'stimulus'

export default class extends Controller {
  connect () {
    this.element[this.identifier] = this
  }

  name () {
    this.element.innerHTML = `I am ${this.element.dataset.name}.`
  }
}

// index.html
<div id="person" data-controller="test" data-name="Steve"></div>

// run this in your console
document.querySelector('#person').test.name()

If everything goes according to plan, the div should now say: I am Steve.


If you want to automatically camelCase the name of your controller, “one-line” becomes dubious, but it can still be done:

    this.element[
      (str => {
        return str
          .split(/[-_]/)
          .map(w => w.replace(/./, m => m.toUpperCase()))
          .join('')
          .replace(/^\w/, c => c.toLowerCase())
      })(this.identifier)
    ] = this

I broke the statement up into multiple lines to help illustrate the acrobatics required to pull this off. It can still be expressed on a single line if you choose. However, the devil hides in clever code.


The only caveat I can think of is that you should exercise common sense and not expose any controller instances that you wouldn’t want people to access. Even though there’s no visible proof that an element has a variable on it in the inspector, you shouldn’t assume that it’s locked down.

If you’re working in FinTech, you might need to skip this technique. Everyone else should be doing this by default.

 
29
Kudos
 
29
Kudos

Now read this

Mutation-first development: a call to action

Not that long ago, someone designing a JavaScript component could rely on a simple life-cycle premise: your content would load before the jQuery embedded in the bottom of the page would come to life and initialize everything that needed... Continue →