Lesson 7 — Cross-controller communication
The challenge
Stimulus controllers are deliberately small and focused. But real UIs often require coordination between controllers — a card being dragged should update a column’s card count; a form submission should trigger a toast; a modal closing should reset a filter.
In ERB, the data-* attributes that wire these interactions are
scattered across templates, making the relationships hard to follow.
In Phlex, each component owns its wiring — cross-controller
communication is more explicit and traceable.
There are three patterns, each suited to different scenarios.
Pattern 1 — Outlets
Outlets let one controller hold a direct reference to another controller instance by CSS selector. Use outlets when two controllers have a tight, permanent relationship and one needs to call methods on the other.
|
|
The outlet is declared via a data-*-outlet attribute pointing to a
CSS selector:
|
|
In Phlex, the outlet wiring lives in the component that needs it.
ToastTrigger declares which element it connects to. ToastContainer
renders with the matching id. The relationship is explicit in both
components rather than scattered across templates.
Pattern 2 — Custom events
Custom events are the most loosely coupled pattern. A controller dispatches a named event; any controller on an ancestor element can listen for it. Use custom events when controllers are independent but occasionally need to notify the broader application.
|
|
|
|
The listener is wired in the HTML using the eventName->controller#method
action syntax:
|
|
KanbanColumn declares that it listens for card:moved events.
KanbanCard declares that it dispatches card:moved. Neither
component needs to know about the other’s internal wiring — the
relationship is visible from each component’s own data: attributes.
This is a meaningful improvement over ERB where these attributes would
be scattered across separate template files.
Pattern 3 — Common ancestor controller
When several child components need to coordinate through a shared parent, put the coordinating controller on the common ancestor element. Child controllers dispatch events upward; the parent orchestrates.
This is the pattern for the KanbanFlow board view:
|
|
In Views::Boards::Show:
|
|
The board div is the common ancestor. It holds data-controller="board"
and listens for card:moved events bubbling up from any KanbanCard
anywhere in the board. KanbanColumn and KanbanCard dispatch events
and let the parent handle coordination — they don’t reference the board
controller at all.
Choosing a pattern
| Situation | Pattern |
|---|---|
| Two controllers always appear together, one calls methods on the other | Outlets |
| Independent controllers that occasionally notify the broader UI | Custom events |
| Multiple children coordinated by a parent | Common ancestor |
Custom events are the most broadly useful — loosely coupled, easy to debug, and require no direct reference between controllers. Reach for outlets only when you need to call a specific method on a specific controller instance. Use the common ancestor pattern for complex coordination like the board view.
Debugging cross-controller communication
Monitor custom events in the browser console:
|
|
The Stimulus DevTools extension shows all active controllers and their outlet connections — invaluable for debugging outlet setup.