Communicating between Stimulus Controllers using Outlets API
Tags: rails, hotwire, webdev, stimulus
Published: 2024-05-23 09:00:00

This article was originally published on [Rails Designer](https://railsdesigner.com/communication-between-stimulus-controllers/).

---

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](https://railsdesigner.com/) too.

Recently I added a [Command Menu Component](https://railsdesigner.com/components/command-menu). 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](https://railsdesigner.com/components/navbars/) or [Sidebar Navigation](https://railsdesigner.com/components/sidebar-navigation/).

I've explored a few options, like [CustomEvents](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events#creating_custom_events).

I've also remembered `this.application.getControllerForElementAndIdentifier()` from the _old days_.

```javascript
  // …

  reloadList() {
    this.listController.reload()
  }

  get listController() {
    return this.application.getControllerForElementAndIdentifier(this.element, "list")
  }
}
```

And there's the option to [use events between controllers too](https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events). But [since November 2022](https://github.com/hotwired/stimulus/pull/576), there's now a more refined option called: [Outlets](https://stimulus.hotwired.dev/reference/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](https://raildesigner.com/components/command-menu/) 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:

```javascript
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:

```html
<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](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors).

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](https://github.com/hotwired/stimulus/issues/669) 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:

```javascript
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.