Zum Inhalt springen Blog Kontakt

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.
 

Diese Artikel könnten Sie auch interessieren

Interesse geweckt?

Alle Artikel anzeigen