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 Stimulus controller, “one-line” becomes dubious, but it can still be done:

    this.element[
      (str => {
        return str
          .split('--')
          .slice(-1)[0]
          .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.

 
131
Kudos
 
131
Kudos

Now read this

Introducing jquery-events-to-dom-events (and jboo)

Did you know that jQuery events aren’t events? It’s true - and it’ll really mess up your night if you need to capture events from legacy jQuery components. Looking at you, hidden.bs.modal. I needed a way to make... Continue →