Communicating between Stimulus Controllers using Outlets API

Abstract, minimalist 3D-like image of a Tin can telephone in summer colors, featuring bright yellow, sky blue, and pale orange, styled with geometric shapes in a modern artistic composition.

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.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months