DEV Community

Jude
Jude

Posted on • Updated on

Stimulus JS - Accessing an action in another controller

If you've used Stimulus before, you should know that controllers are scoped. This can be useful, but you might come across a case where we want to trigger an action in a controller that is out of scope.

We can do this with a custom event.

I'll start off with a tl;dr version and will follow with an explanation

TL;DR

// The javascript
const trigger = new CustomEvent("event-name");
window.dispatchEvent(trigger);
Enter fullscreen mode Exit fullscreen mode
<!-- The HTML attribute -->
data-action="event-name@window->controller#action"
Enter fullscreen mode Exit fullscreen mode

Explanation

Here's a contrived example, to give you an idea of how to get this working. Here are some circles we want to turn red when we click a button.

<div data-controller="controller1">
  <button data-action="click->contoller1#turnCircleRed">
    Button 1
  </button>
  <div 
    class="round-square" 
    data-controller1-target="circle">
  </div>    
</div>

<div data-controller="controller2">
  <button data-action="click->contoller2#turnCircleRed">
    Button 2
  </button>
  <div 
    class="round-square" 
    data-controller2-target="circle">
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Now let's create the controllers

// app/javascript/controllers/controller1
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="controller1"
export default class extends Controller {
  static targets = [ "circle" ]

  turnCircleRed() {      
    this.circleTarget.style.backgroundColor = "red";
  }
}
Enter fullscreen mode Exit fullscreen mode

and here's the second one.

// app/javascript/controllers/controller2
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="controller2"
export default class extends Controller {
  static targets = [ "circle" ]

  turnCircleRed() {      
    this.circleTarget.style.backgroundColor = "red";
  }
}
Enter fullscreen mode Exit fullscreen mode

Now when we click on either of the buttons, the background of the circle next to the button will turn red. Even though both targets are called "circle", because each stimulus controller only has access to elements inside of the div (or element) where it was declared, it cannot see or interact with the other circle.

Now, what we want to happen is that when we click on the second button, we want BOTH circles to turn red.

To do this, we add a custom event to controller2's turnRed action.

// app/javascript/controllers/controller2
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="controller2"
export default class extends Controller {
  static targets = [ "circle" ]

  turnRed() {        
    this.circleTarget.style.backgroundColor = "red";

    // Custom event we will use as our trigger
    const trigger = new CustomEvent("trigger-red");
    window.dispatchEvent(trigger);
  }
}
Enter fullscreen mode Exit fullscreen mode

and to our view, we want to add an action that will listen for this event.

<div data-controller="controller1">
  <button data-action="click->contoller1#turnRed">
    Button 1
  </button>
  <div id="circle1" class="round-square" 
    data-controller1-target="circle" 
    data-action="trigger-red@window->contoller1#turnRed"> 
  </div>
</div>

<div data-controller="controller2">
  <button 
    id=circle2" 
    class="round-square" 
    data-action="click->contoller2#turnRed"
  >
    Button 2
  </button>
  <div class="round-square" data-controller1-target="circle">
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Now when we click on Button 2 the following will happen

  1. controller2#turnRed action will run and #circle2 will turn red.
  2. Then an event called "trigger-red" will be attached to the "window" element
  3. "trigger-red@window" will see this, and run controller1#turnRed

You can take this further and have an action that only adds the custom event with listeners that will trigger turnRed in both controllers. This would allow you to add another button to turn the circles blue, using another event you could call "trigger-blue" that would trigger "turnBlue" actions, fairly easily.

A mild word of warning

Now we know how to do this, I think it's a good idea to use this sparingly as it does add some complexity to our code and makes us use a little more mental gymnastics to read and update our code in the future.

Let me know if this was helpful, if you have any questions, or spotted some mistakes/errors or got something completely wrong.

Thanks for reading, and happy coding!


References

https://stimulus.hotwired.dev/handbook/introduction

Top comments (2)

Collapse
 
haroldmeza profile image
haroldmeza

Nice tip, i used the communication between controllers through dispatch, but this feature has a weird inconvenient, you lost this context when you wanna communicate with other component, so you can't access the real context in the receiver component, but using window.dispatchEvent, you don't lose this context in the reciever component, thanks, you make my day

Collapse
 
katekostina profile image
Kate Kostina

Thanks, that was helpful!