Komponenten sind der Grundbaustein einer jeden Svelte-Anwendung. Sie enkapsulieren spezifisches Aussehen und/oder Verhalten. Wie in anderen JavaScript-Frameworks auch lassen sich aus einzelnen Komponenten immer komplexere und mächtigere Komponenten erstellen. Die Best Practices sind dabei im Prinzip ähnlich zur sonstigen Programmierung – Wiederverwendbarkeit, Single Responsibility Principle, und alles möglichst pragmatisch.
In diesem Blogeintrag schauen wir uns die verschiedenen APIs an, die uns Svelte zur Verfügung stellt, um Komponenteninteraktion bestmöglich zu implementieren.
Andere Komponenten verwenden
Codeorganisation beginnt damit, für jeden Code den passenden Ort zu finden. Zwangsläufig wird dies in verschiedenen Komponenten resultieren – eine Anwendung in eine riesige Gottkomponente zu pressen, skaliert nicht und ist nicht wartbar. Komponenten sollten also andere Komponenten verwenden, um Aufgaben zu delegieren. Syntaktisch sieht dies wie ein ganz normaler Import aus: Eine Svelte-Komponente wird als Default Import im <script>
-Tag importiert, der Name des Imports kann dann als regulärer Tag im Template verwendet werden:
Input-Properties
Viele Komponenten benötigen Input-Properties, um korrekt zu funktionieren. Das folgende Beispiel zeigt eine simple Komponente, die eine hereingegebene Liste rendert. Diese Input-Properties werden über export let <Property-Name>
innerhalb des <script>
-Tags definiert:
Diese Syntax mag auf den ersten Blick etwas gewöhnungsbedürftig erscheinen, wird aber schnell in Fleisch und Blut übergehen.
Properties ohne Default-Wert müssen auf Verwenderseite gesetzt werden. Optionale Properties haben einen Wert angegeben, der genutzt wird, falls die Verwenderseite diese Property nicht verwendet. In unserem Beispiel erweitern wir die Liste um eine optionale Property, um bei Bedarf eine Löschfunktion für die Einträge einzublenden.
Die Verwendung dieser Komponente mit beiden Input-Properties sieht dann folgendermaßen aus:
Auf Events reagieren
Der Löschen-Knopf in unserem Beispiel ist nun zwar vorhanden, wenn showRemove
auf true
gesetzt ist, aber er löst noch nichts aus. Die Liste innerhalb der Komponente zu aktualisieren, wäre schlechter Stil, also benötigen wir eine Art Event, um die Verwenderkomponente zu benachrichtigen, etwas zu tun. Hierfür haben wir in diesem Beispiel zwei Optionen.
Die erste Option ist, mit sogenannten Callback-Properties zu arbeiten, wie man sie etwa aus React kennt. Man definiert also ein Input-Property, welches eine Funktion erwartet, und ruft diese dann auf:
{items}
ist Kurzschreibweise für items={items}
– eines von vielen kleinen Dingen, die das Arbeiten mit Svelte so kurzweilig macht.
Die zweite Option ist, einen sogenannten Event Dispatcher zu erstellen und darüber eine Benachrichtigung zu senden. Hierfür nutzen wir die Methode createEventDispatcher
und rufen den hiermit erstellten Dispatcher entsprechend auf. Auf Verwenderseite hören wir auf das Event wie auf normale DOM-Events mit Sveltes on:<Event-Name>
-Syntax.
Daneben gibt es noch die Möglichkeit, Events einfach weiterzuleiten. Dies erreichen wir, indem wir on:<Event-Name>
ohne darauffolgenden Event-Handler schreiben.
In unserem Beispiel macht dies jedoch keinen Sinn, da wir auch die Information mitgeben müssen, welches Element genau gelöscht werden soll; außerdem ist der Event-Name click
zu generisch.
UI und Verhalten durch Slots von außen definieren
Unsere Liste kann nun Einträge und optional einen Löschen-Knopf anzeigen. Das Aussehen eines einzelnen Listeneintrags können wir allerdings nicht bestimmen. Außerdem möchten wir vielleicht je nach Anwendungsfall andere Dinge außer einem Löschen-Knopf anzeigen. All dies in einer generischen Listen-Komponente über Input-Properties abzubilden, skaliert irgendwann nicht mehr. Hier kommen uns Slots gelegen, die es uns ermöglichen, in der Verwender-Komponente Teile der Listen-Komponente komplett frei zu definieren – und zwar sowohl was das Aussehen, als auch was den Inhalt angeht.
Nutzen wir diesen Umstand, um unsere Listen-Komponente zu ihrem Ursprung zurückzuführen – eine Liste anzeigen. Das Aussehen und Verhalten eines Eintrags überlassen wir jedoch der Verwenderseite:
Ein <slot>
-Tag markiert einen Bereich als vom Verwender befüllbar. Die {item}
– und {idx}
-Properties auf dem <slot>
-Tag bedeuten, dass der Verwender diese Variablen verwenden kann, um seine UI zu definieren – wir reichen sie quasi „nach oben“. Alles innerhalb des <slot>
-Tags ist der Fallback-Content, falls der Verwender diesen Slot nicht befüllt. In unserem Fall zeigen wir einfach nur den Text des Items an.
Den Slot zu befüllen, sieht nun folgendermaßen aus:
Der Slot wird befüllt durch alles, was innerhalb des <List>
-Tags steht. Mit let:<Variablenname>
wird auf die Properties zugegriffen, die die Listen-Komponente „nach oben“ gereicht hat. Wenn wir den Variablennamen umbenennen wollten, so könnten wir dies mit let:<Variablenname>={<Anderer-Variablenname>}
tun.
Der eben gesehene Slot ist der sogenannte Default Slot. Daneben können wir noch Named Slots nutzen, falls wir mehrere Platzhalter definieren wollen. Sie zu definieren, ist genauso einfach, nur dass wir <slot name="<gewünschterName>">
innerhalb der Komponente schreiben, und auf der Verwenderseite analog auf diesen Slot zugreifen:
Das folgende Bild visualisiert das Slot-Feature nochmals:
Module Script für instanzübergreifende Kommunikation
Manchmal ist es sinnvoll, über mehrere Instanzen einer Komponente hinweg zu kommunizieren. Nehmen wir das Beispiel einer Audioplayer-Komponente – es sollte immer nur ein Audioplayer gleichzeitig Musik spielen. Wie erreichen wir das in Svelte, wenn mehrere gleichzeitig auf einer Seite zu sehen sind?
Hier kommt uns <script context="module">
gelegen. Dieses zweite Script-Tag wird einmal zu Beginn ausgeführt und ist dann über alle Instanzen der Komponente hinweg persistent – ein bisschen wie eine globale Datei. In unserem Beispiel können wir das nun nutzen, um zu speichern, ob gerade ein Audioplayer läuft, und diesen stoppen, falls ein anderer gestartet wird:
Diese Logik ist somit schön nah bei der Komponente enkapsuliert, und wir müssen uns bei der Verwendung keine Gedanken darüber machen:
Das folgende Bild veranschaulicht das Module-Script-Feature nochmals.
Globale Werte
Wir haben gesehen, wie wir Properties für Komponenten definieren und zuweisen können, und wie wir innerhalb einer Komponente instanzübergreifend kommunizieren können – aber wie sieht es aus, wenn wir Werte für unsere gesamte Anwendung setzen möchten? Nehmen wir zum Beispiel den Login-Status – er hat Auswirkungen auf unsere gesamte Anwendung und wird an vielen Stellen benötigt, entweder um ihn zu verändern oder abzufragen. Svelte lässt uns in dieser Hinsicht freie Hand: Was global ist, können wir entsprechend in einer globalen Datei verwalten, und alle relevanten Komponenten können die Variablen und Methoden aus dieser regulär importieren und verwenden.
Wie schaffen wir es jedoch, auf Statusänderungen innerhalb der Komponente zu reagieren, sodass diese sich neu rendern? Hierfür bieten sich Stores an, die Svelte gleich mitliefert. Sogenannte writables
enkapsulieren einen Zustand in, der mit den Methoden set
und update
geändert werden kann; mit subscribe
können wir auf die Änderungen horchen. Im Code sieht das Ganze dann so aus:
Wer sich nun wundert, wo denn eigentlich der subscribe
-Aufruf innerhalb der Komponente ist – Svelte spart uns hier mit einem Trick den Boilerplate-Code. $loggedIn
enthält den aktuellen Wert des Stores. Generell ist jede Top-Level-Variable, der ein Dollarzeichen vorangestellt ist, eine automatische Store-Subscription. Da Svelte ein Compiler ist, kann es zur Buildzeit den benötigten Code generieren. Würden wir das Ganze „von Hand“ selbst schrieben, sähe der Code ungefähr so aus:
Schön, dass Svelte uns diese Arbeit abnimmt! Übrigens ist man mit dieser Syntax nicht auf Sveltes Stores begrenzt – jedes Objekt, welches eine subscribe
-Methode bereitstellt, kann auf diese Weise verwendet werden. Wer also zum Beispiel lieber RxJS-Observables verwenden möchte, kann dies problemlos tun.
Kontext für bereichsweite Werte
Wir haben gesehen, wie wir globale Werte verwalten können. Ein globales Feature Flag zum Beispiel können wir in einer zentralen Datei definieren und dann überall importieren. Doch was machen wir, wenn das Feature Flag je nach Bereich an oder aus sein soll? Globale Werte können wir dann nicht mehr verwenden. Eine Möglichkeit wäre, sogenanntes Property-Drilling zu betreiben und das Feature Flag an den passenden Stellen in den Komponenten zu definieren und dann durch alle Zwischenkomponenten an ihr Ziel durchzureichen. Dies koppelt jedoch unbeteiligte Komponenten an das Feature Flag, und ist generell schnell lästig. Glücklicherweise hat Svelte auch für dieses Problem eine Lösung parat: den Kontext.
Zunächst nutzen wir setContext
, um einen neuen Kontext aufzumachen. setContext
erwartet einen Key, unter dem der Kontext gefunden werden kann, und den zugehörigen Wert – für beides ist vom einfachen String bis zum komplexen Objekt alles erlaubt. In unserem Beispiel nutzen wir dies, um ein „FancyLists“-Feature-Flag zu setzen:
Mit getContext
können wir nun in allen Kindkomponenten auf diesen Kontext zugreifen und ihn innerhalb der Komponente wie eine normale Variable nutzen:
Um das Ganze etwas zu enkapsulieren, empfiehlt es sich, das Setzen und den Zugriff an einer Stelle zu verwalten:
setContext
und getContext
innerhalb unserer Komponenten können wir dann mit den eben definierten Methoden ersetzen. Die einzige Regel, die es bei der Verwendung zu beachten gibt, ist, dass die Kontext-Methoden während der Komponenteninitialisierung aufgerufen werden müssen. setContext
sollte also nicht zum Aktualisieren eines Wertes genutzt werden. Stattdessen ist die Kombination aus setContext
und den im vorherigen Abschnitt eingeführten Stores der beste Weg, einen sich verändernden Wert zu definieren. Wir geben also die Instanz eines writables
in setContext
hinein, …
… greifen mit getContext
wieder auf sie zu, und nutzen erneut die Dollarsyntax zu unserem Vorteil, um mit $fancyLists
auf den Store zu subscriben.
Das folgende Bild veranschaulicht das Kontext-Feature nochmals.
Komponenteninteraktion mit Svelte – Fazit
Wie wir gesehen haben, bietet Svelte für jede Art der Komponenteninteraktion das passende Werkzeug. Für die direkte Kommunikation setzen wir Properties und hören auf Events. Slots geben uns mehr Freiheiten, sowohl was den Inhalt, als auch was das Aussehen angeht. Das Module-Script können wir für die Kommunikation zwischen verschiedenen Instanzen gleichen Typs verwenden. Globales State Management ist durch Sveltes Stores einfach möglich – und kann bei Bedarf um eigene Lösungen erweitert werden. Bereichsweite Werte verwalten wir über das Kontext-Feature. So ausgerüstet steht einer robusten und wartbaren Entwicklung mit Svelte nichts mehr im Wege.
- Svelte 5 – ein Blick auf die neuen APIs - 28. Oktober 2024
- Svelte – alles, was Sie über Komponenteninteraktion wissen müssen - 1. August 2023
- Eine Einführung in das JavaScript-Framework Svelte – mehr erreichen mit weniger Code - 17. Dezember 2020