Stimulus Discourse

Run an action on every other controller instance


#1

I have a bunch of instances of controllers X and Y. What I would like to achieve now, is to be able to call a method, let’s call it onDeFocus, on every other instance of these controllers, from a controller this would be triggered in, with a click action. I do have all the X and Y controllers prefixed with blocks--, if that makes it any easier.

Is there a way to achieve this somehow?


#2

You could rely on a parent controller to handle this:

<div data-controller="parent">
  <div data-controller="blocks--x" data-action="click->parent#unfocusSiblings" data-target="parent.child">...</div>
  <div data-controller="blocks--y" data-action="click->parent#unfocusSiblings" data-target="parent.child">...</div>
</div>
// parent_controller.js
unfocusSiblings(event) {
  let siblings = this.childTargets.filter(child => {
    return child !== event.currentTarget 
  })

  for (let sibling of siblings) {
    // Do what you need to do
  }
}

An advantage of this approach is the reduced coupling between controllers. It doesn’t care about the implementation of X and Y, as long as they have compatible HTML tags. It relies on the DOM instead of controller instances.

If you do need to handle this through the controllers though, you can:

<div data-controller="blocks--x" data-action="click->blocks--x#unfocusSiblings">...</div>
<div data-controller="blocks--y" data-action="click->blocks--y#unfocusSiblings">...</div>
<div data-controller="blocks--x" data-action="click->blocks--x#unfocusSiblings">...</div>
<div data-controller="blocks--y" data-action="click->blocks--y#unfocusSiblings">...</div>
// x_controller.js or y_controller.js
unfocusSiblings() {
  for (let sibling of this.siblingControllers) {
    sibling.onDeFocus();
  }  
}

get siblingControllers() {
  return this.application.controllers.filter(controller => {
    return controller.identifier.startsWith('blocks--') && controller !== this
  })
}

#3

I had some additional thoughts about the implications of this design. Feel free to disregard them if you don’t find them useful.

While it’s tempting to use this pattern, be aware that it might result in brittle code and get increasingly difficult to reason about over time.

Stimulus relies on a descriptive DOM. That’s why the descriptors are so verbose. You should have an idea about what will happen by looking at the DOM.

Letting controllers call methods on each other obfuscates this principle. You tighten the coupling by relying on controller identifiers and by making assumptions about which methods they implement.

It should also be noted that fetching other controller instances is undocumented behavior. There is currently no documented way to aid communication between controllers (although a solution might be on its way).

In your specific case, you have controller instances calling methods on other instances of their own type (or types with a common interface). You could consider leveraging this fact and extract the shared functionality to a parent controller:

// blocks/block_controller.js
import { Controller } from "stimulus"
export class BlockController extends Controller {
  unfocusSiblings() {
    // ...
  }

  get siblingControllers() {
    // ...
  }
}

// blocks/x_controller.js
import { BlockController } from "./block_controller"
export default class extends BlockController { 
  // ...
}

// blocks/y_controller.js
import { BlockController } from "./block_controller"
export default class extends BlockController { 
  // ...
}

This makes it easier to maintain, should the methods and identifier change over time.