2025-04-28
Stimulus outlets
Stimulus Outlets is a superful useful feature that allows one controller talk or interact with another controller that lives elsewhere in the page — even if they are not nested together.
This can be really helpful when you want components on your page to interact without tightly coupling their HTML structure.
Let’s say a user is browsing through products on the /products
page. Here’s a simplified example:
<!-- products/index.html.erb -->
<div class="mx-auto max-w-2xl mt-24" data-controller="cart" data-cart-cart-notification-outlet=".cart-count">
<div class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
<% 4.times do %>
<a href="#" class="group">
<img src="https://tailwindcss.com/plus-assets/img/ecommerce-images/category-page-04-image-card-01.jpg" class="aspect-square w-full rounded-lg bg-gray-200 object-cover group-hover:opacity-75 xl:aspect-7/8">
<h3 class="mt-4 text-sm text-gray-700">Earthen Bottle</h3>
<p class="mt-1 text-lg font-medium text-gray-900">$48</p>
<button class="mt-1 bg-blue-700 hover:bg-blue-600 flex py-2 w-full rounded-4xl justify-center text-white font-light cursor-pointer text-sm" data-action="cart#add:prevent">Add to cart</button>
</a>
<% end %>
</div>
</div>
// cart_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static outlets = [ "cart-notification" ]
add() {
if (this.hasCartNotificationOutlet) {
this.cartNotificationOutlet.updateCount()
}
}
}
Whenever a user clicks “Add to cart,” we want to update the cart count badge. However, the cart badge is rendered separately — inside the layout, not inside the /products
page itself.
<!-- layouts/application.html.erb -->
<body class="min-h-full flex flex-col">
<div class="flex-1">
<%= yield %>
<%= render "layouts/cart_count" %>
</div>
</body>
<!-- layouts/_cart_count.html.erb -->
<div class="cart-count" data-controller="cart-notification">
<div class="hidden fixed bottom-5 right-7" data-cart-notification-target="container">
<div class="relative py-2">
<div class="t-0 absolute left-3">
<p class="flex h-2 w-2 items-center justify-center rounded-full bg-red-500 p-3 text-xs text-white" data-cart-notification-target="itemsCount"></p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="file: mt-4 h-6 w-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
</div>
</div>
</div>
// cart_notification_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["container", "itemsCount"]
connect() {
this.cartItems = 0
}
updateCount(event) {
if (this.containerTarget.classList.contains("hidden") && this.cartItems == 0) {
this.containerTarget.classList.remove("hidden");
};
this.cartItems += 1;
this.itemsCountTarget.innerText = this.cartItems;
}
}
Since the cart controller (cart_controller.js
) lives in the /products
page and the cart notification controller (cart_notification_controller.js
) lives globally inside the layout, outlets allow the cart controller to find and update the cart notification controller easily — without needing to nest them together or manually query the DOM.
Similar to Targets, Outlets have callbacks that allow you to run code whenever an outlet is connected or disconnected from the page.
This is incredibly powerful because it lets your controller react dynamically as the structure of the page changes — without needing to manually observe the DOM yourself.
For example, imagine you’re building a chat application. When a user receives a new incoming message like “Happy Birthday!”, you could have that message element matched by an outlet selector like .celebration
. When the outlet connects, you could automatically trigger a fun confetti animation or display a special effect — all driven by the outlet connection itself, without any extra manual wiring.
Some other use cases where outlets might be useful(although most of these could also be acheived through ruby):
A form wizard with a current-step navigation bar - Inform the user what current step they are on.
Dynamic sidebars - Selecting something in the main content area updates sidebar content dynamically.
Modal openers - Buttons that open modals “from far away” without having the modal HTML nested inside the button.
Flash notifications - A form submits → triggers a global “success” notification banner to appear.