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);
// or js inline
<button onclick="window.dispatchEvent(new Event('event-name'))"> Button </button>
<!-- The HTML attribute -->
data-action="event-name@window->controller#action"
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>
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";
}
}
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";
}
}
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);
}
}
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>
Now when we click on Button 2 the following will happen
- controller2#turnRed action will run and #circle2 will turn red.
- Then an event called "trigger-red" will be attached to the "window" element
- "trigger-red@window" will see this, and run controller1#turnRed
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!
Top comments (2)
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
Thanks, that was helpful!