43 · side path · outlets

One controller reaching into others.

<div data-controller="chat"
     data-chat-user-status-outlet=".online-user">
  <button data-action="chat#selectAll">select all</button>
</div>

<div class="online-user" data-controller="user-status">…

// chat_controller.js
static outlets = [ "user-status" ]
selectAll() {
  this.userStatusOutlets.forEach(u => u.markAsSelected())
}

An outlet is a target that points at another controller, found by CSS selector anywhere on the page, not just inside your element. You get the controller instance, so you can call its methods, read its values, touch its targets. Press select all.

43 · the looser option

Or just shout, and let others listen.

// user_status_controller.js
select() {
  this.element.classList.add("selected")
  this.dispatch("selected", { detail: { id: this.idValue } })
}

<!-- the chat controller listens for user-status:selected -->
<div data-controller="chat"
     data-action="user-status:selected->chat#noteSelection">

this.dispatch("selected") fires a DOM event named user-status:selected that bubbles. Any controller above it can catch it with an ordinary action. No selector, no reference, no coupling. Click a user on the right; the chat controller hears about it.

Outlets when you need to command other controllers; dispatch when you need to announce. Most apps need dispatch more often.

43 · scope

A controller only sees its own targets.

Two list controllers, one nested inside the other. Hover each to see which item targets it owns. The outer one does not see the inner one's items, even though they're inside its element. Nested scopes stop at the next controller with the same identifier.

That's what lets you drop the same controller on a page ten times, or inside itself, without them stepping on each other. It's also why an outlet exists at all: to reach across a scope boundary on purpose.