Stimulus is a great, modest JavaScript framework to add those joyful little sprinkles of JavaScript to your (Rails) web application.
I’ve been embracing it almost from the day of release for all my Rails apps and it’s a core tool of Rails Designer too.
Recently I added a Command Menu Component. It’s the typical one that can be displayed with a CMD/Ctrl+K
shortcut (or whatever you choose). The component is supposed to be added to your application layout so it can be used by your users at any time.
I also wanted the option to display it upon clicking some button, think a “search” field in a NavbarComponent or Sidebar Navigation.
I’ve explored a few options, like CustomEvents.
I’ve also remembered this.application.getControllerForElementAndIdentifier()
from the old days.
// …
reloadList() {
this.listController.reload()
}
get listController() {
return this.application.getControllerForElementAndIdentifier(this.element, "list")
}
}
And there’s the option to use events between controllers too. But since November 2022, there’s now a more refined option called: Outlets.
Use the Outlets API
The solution I needed! Let’s check the code to have one Stimulus controller communicate with another Stimulus controller.
The Command Menu Component comes with two Stimulus controllers:
command_menu_controller.js
; it’s an advanced Stimulus controller, but no changes needed here for Outlets to work;command_menu/button_controller.js
; this is the extra stimulus controller needed. I’ve nested it under/command_menu/
as the two are highly related.
The command_menu/button_controller.js
looks like this:
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static outlets = ["command-menu"];
open() {
this.commandMenuOutlet.open();
}
}
Simple! It defines the controller identifiers in the static outlets
array. That is then used in the open()
function. The open()
function then calls the function that should be called on the other controller, which incidentally is also called open()
.If you are familiar with Stimulus this doesn’t look weird.
And it’s used in your HTML like this:
<button data-controller="command-menu--button" data-command-menu--button-command-menu-outlet="#command-menu" data-action="command-menu--button#open">
Show Command Menu
</button>
The nesting of the controller make for a bit confusing syntax (double dashes), but if you’d mentally replace the double-dash with a slash, it’s more understandable.
The important part for Outlets to work is this attribute: data-command-menu--button-command-menu-outlet="#command-menu"
. The value should be a CSS selector.
If we check the relevant HTML part, you can see an id
of command-menu
:
<div id="command-menu" data-controller="command-menu">
<!-- … -->
</div>
Outlet names must be the same as the controller name
Let me repeat that: Outlet names Must be The Same As The Controller Name.
Many developers have lost hours on this; yours truly included. I remembered when using it for the Command Menu’s button controller, but if you don’t use outlets enough, you surely will get bitten by it again.
In command_menu/button_controller.js
static outlets
array you see command-menu
is the same as the controller name it communicates with (command_menu_controller.js
).
The CSS selector’s value (#command-menu
) to “find” the controller can be anything as long as the Outlet Name Is The Same As The Controller Name!
With the outlets set up correctly in the command_menu/button_controller.js
you now have access to the actual Controller instance from command_menu_controller.js
. This means you can use:
- values (
static values
); - classes (
static classes
); - targets (
static targets
).
So whatever you do in the command_menu_controller.js
you can now also do in the command_menu/button_controller.js
.
You are also not limited to one “instance” of a controller. Just like with “targets”, you can have multiple outlets. So you can write this:
export default class extends Controller {
static outlets = ["user-status"];
selectAll(event) {
this.userStatusOutlets.forEach(status => status.markAsSelected(event));
}
}
And that’s the Outlets API. If you don’t forget that the outlet name must be the same as the controller name, it’s a great way to increase what you can do with Stimulus.