Wer bereits mit dem Javascript Framework Stimulus arbeitet, kennt und schätzt die Einfachheit von Targets. Sie sind eines der zentralen Konzepte, um DOM-Elemente innerhalb eines Controllers sauber zu adressieren. Doch spätestens dann, wenn mehrere Controller miteinander interagieren sollen, stoßen Targets an eine konzeptionelle Grenze. Und genau hier kommen Outlets ins Spiel.
Zugegeben: Outlets sind kein brandneues Feature, aber lange fehlten mir überzeugende Einsatzszenarien, um sie im Alltag wirklich zu nutzen. Dieser Artikel soll zeigen: Die Beschäftigung mit Outlets lohnt, stellen diese schließlich einen durchdachtes Weg dar, um Controller miteinander kommunizieren zu lassen – ohne globale State-Container, ohne Event-Gefrickel und ohne querySelector-Akrobatik.
Targets vs. Outlets – der entscheidende Unterschied
Zunächst eine klare Abgrenzung:
Targets:
- referenzieren DOM-Elemente
- müssen innerhalb des Controller-Elements liegen
- bieten keinen Zugriff auf andere Controller
- eignen sich hervorragend für interne UI-Logik
Outlets:
- referenzieren andere Controller
- diese Controller können irgendwo im DOM liegen
- bieten Zugriff auf
- das DOM-Element
- den Controller selbst
- ermöglichen direkte Methodenaufrufe zwischen Controllern
Man kann es so zusammenfassen:
Targets sind für Struktur innerhalb eines Controllers gedacht, Outlets für Kommunikation zwischen Controllern.
Vorteile von Outlets
Der eigentliche Mehrwert von Outlets liegt im State- und Logikaustausch zwischen Controllern.
Vor Outlets gab es dafür im Wesentlichen drei Ansätze:
- globale State-Management-Lösungen
- Custom Events
- manuelles Suchen anderer Controller per DOM-Selektoren
All das funktioniert, fühlt sich aber nie wirklich elegant oder „Stimulus-ish“ an. Outlets schließen diese Lücke auf sehr konsistente Weise.
Praxisbeispiel: Produktliste & Warenkorb
Wir stellen uns ein klassisches Szenario aus dem E-Commerce vor:
- Ein Produktkatalog in dem jedes Produkt einen eigenen
product-Controller besitzt - Einen Warenkorb, der einen eigenen
cart-Controller hat und im Header sitzt - Einen Button „Zum Warenkorb hinzufügen“ bei dessen klick die Anzahl der Produkte im Warenkorb hochgezählt werden soll
Das könnte etwa so aussehen:
<!-- Warenkorb -->
<header>
<div data-controller="cart">
<span class="visually-hidden">Produkte im Warenkorb:</span>
<span data-cart-target="count">0</span>
</div>
</header>
<!-- Produkt(e) -->
<div
data-controller="product"
data-product-cart-outlet="[data-controller='cart']"
data-product-product-value='{"id": 1, "name": "Robole Hoodie flieder"}'
>
<button data-action="click->product#onClickAddToCart">
Zum Warenkorb hinzufügen
</button>
</div>
Die Verlinkung vom Product- zum Cart-Controller geschieht in diesem Beispiel über das Attribut data-product-cart-outlet="[data-controller='cart']". Der Wert dieses Attributs entspricht dabei einem regulären CSS-Selektor. Sollten also mehrere Instanzen eines Outlet-Controllers existieren, so lassen sich diese etwa anhand ihrer Klasse (.cart) referenzieren. Der Attribut-Schlüssel entspricht dem Muster data-[identifier]-[outlet]-outlet, wobei [outlet] dem exakten Namen des Outlet-Controllers entspricht.
Der Product-Controller
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static outlets = ['cart'];
static values = {
product: Object
};
onClickAddToCart() {
if (!this.hasCartOutlet) {
console.warn('Kein Cart-Outlet gefunden');
return;
}
this.cartOutlet.addToCart(this.productValue);
}
}
Durch static outlets = ['cart'] erzeugt Stimulus automatisch die folgenden Eigenschaften:
hasCartOutlet: boolean;
cartOutlet: Controller;
cartOutlets: Controller[];
cartOutletElement: HTMLElement;
cartOutletElements: HTMLElement[];
Damit wird sofort klar:
- Outlets können einzeln oder mehrfach existieren
- Man kann gezielt prüfen, ob ein (oder mehrere) Outlet verfügbar ist
- Man arbeitet direkt mit dem Controller, nicht nur mit dem DOM
Der Cart-Controller
Schauen wir uns nun den Warenkorb-Controller an:
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['count'];
connect() {
this.items = [];
}
addToCart(product) {
this.items.push(product);
this.countTarget.textContent = this.items.length;
}
}
Der cart-Controller stellt mit addToCart eine klar definierte, öffentliche API bereit. Genau dafür sind Outlets gedacht!
Outlet Callbacks: Lifecycle-Events im Blick behalten
Da Stimulus intern auf MutationObserver basiert, behalten Outlets automatisch im Blick, ob Controller erscheinen oder verschwinden. Das ist besonders interessant bei asynchron gerenderten Inhalten oder Page Transitions (z. B. mit Swup oder Turbo). Stimulus erzeugt automatisch die folgenden Lifecycle Callbacks:
cartOutletConnected(outlet) {
// wird aufgerufen, wenn der Cart-Controller verfügbar wird
}
cartOutletDisconnected(outlet) {
// wird aufgerufen, wenn der Cart-Controller entfernt wird
}
Fazit
Outlets schließen eine lange bestehende konzeptionelle Lücke in Stimulus:
- klare Controller-Grenzen
- explizite Abhängigkeiten
- kein implizites DOM-Suchen
- kein globaler State
- kein Event-Wildwuchs
Wer Stimulus bereits produktiv einsetzt, sollte Outlets unbedingt ausprobieren! Gerade bei komplexeren Frontends – etwa in Symfony-Anwendungen auf Basis des StimulusBundle – sorgen sie für deutlich besser strukturierte und wartbare Architektur.
Weitere Details und Spezialfälle finden sich in der offiziellen Dokumentation zu Stimulus Outlets.