Für die Umsetzung des Frontends einer Webanwendung war in den letzten Jahren meistens eine Single-Page-Anwendung (SPA) erste Wahl. Diese Art komplett im Browser laufendes JavaScript, das per JSON mit dem Backend spricht, hatte in kurzer Zeit alles andere überrannt. Doch seit Kurzem scheint das Pendel wieder in die andere Richtung zu schwingen, denn bei SPAs entsteht eine Menge an JavaScript.

Natürlich darf kein Trend ohne Bibliotheken und Frameworks auskommen. Mit htmx gibt es aktuell eine solche Bibliothek, welche uns verspricht, beim Bau moderner Frontends mit einem simplen und deklarativen Ansatz zu unterstützen. Deshalb wollen wir uns hier einmal anschauen, was htmx zu bieten hat.

htmx

Das Kernziel von htmx ist es, die bereits in HTML vorhandenen Eigenschaften von Hypertext zu erweitern. Hypertext als Kern des World Wide Web (WWW), wie wir es heute kennen, ist dabei der Teil, der es uns erlaubt, zwischen den zahlreichen Dokumenten im WWW zu springen. HTML unterstützt dies durch Links und Formulare. Links sind dabei auf die Nutzung des HTTP-Verbs GET beschränkt. Formulare unterstützen neben GET auch noch POST als Verb. Beide führen dabei zum Laden eines neuen Dokuments und damit auch zu einem kompletten Neuzeichnen der Seite im Browser. Zur Ausführung der Aktion muss entweder der Link geklickt oder per Tastatur ausgelöst werden oder das Formular muss abgeschickt werden.

Genau hier setzt htmx an und möchte es uns ermöglichen, auch andere HTML-Elemente zur Hypertextnavigation zu verwenden. Dabei sollen uns auch die nicht von HTML unterstützten HTTP-Verben, wie DELETE oder PUT, zur Verfügung stehen. Und auch die Möglichkeiten, diese Aktion auszulösen, werden erweitert. Zuletzt wird dabei noch, als Optimierung, unterstützt, dass nur Teile der aktuellen Seite durch das Ergebnis der Aktion ersetzt werden und somit nicht zwangsweise die ganze Seite neu gezeichnet werden muss. Dabei wird standardmäßig davon ausgegangen, dass der Server mit HTML antwortet und nicht, wie bei SPAs üblich, mit JSON.

Als Programmiermodell setzt htmx primär darauf, dass das vom Server generierte HTML deklarative Anweisungen, mithilfe von Attributen, enthält. Im Browser wird dann die, minifiziert um die 16 KByte große, JavaScript-Bibliothek hinzugefügt Diese kommt ohne eigene Abhängigkeiten daher und bietet uns neben den Kernfunktionen auch einen Erweiterungsmechanismus an.

Doch beginnen wollen wir, wie es sich gehört, am Anfang und damit bei Ajax, der Kernfunktionalität von htmx.

Ajax

Ajax, Asynchronous JavaScript and XML, gibt es bereits seit den 90ern und bezeichnet die Technik, im Browser mittels JavaScript dynamisch Inhalte nachzuladen oder zu aktualisieren, ohne die gesamte Seite neu zu laden. Da das JavaScript hierbei im Hintergrund, asynchron, ausgeführt wird, bleibt die Seite auch während der Aktion benutzbar. Im Grunde nutzen also auch die modernen SPAs diese Funktionalität, auch wenn wir in diesem Zusammenhang den Begriff Ajax in der Regel nicht mehr nutzen.

Aber htmx hat sich dazu entschlossen, seine Kernfunktionalität mit diesem Begriff zu bezeichnen. Um diese Funktionalität zu nutzen, reicht uns bereits, neben dem Einbinden der Bibliothek, ein einzelnes Attribut, wie in Listing 1 zu sehen. Klicken wir nun auf dieses div-Element, wird eine HTTP-GET-Anfrage an den Uniform Resource Identifier (URI) /employees gesendet und der Inhalt des geklickten Elements wird durch das Ergebnis ersetzt (s. Abb. 1).

...
<div hx-get="/employees">
    Show all employees
</div>
...
Listing 1: htmx-Ajax-GET-Request nach Klick auf div
Abb. 1: HTTP-Antwort auf die htmx-GET-Anfrage

Um ein anderes HTTP-Verb für die Anfrage zu nutzen, stehen uns mit hx-post, hx-put, hx-patch und hx-delete noch vier weitere Attribute zur Verfügung, die sich ansonsten identisch zu hx-get verhalten. Das Nachladen und Ersetzen geschieht dabei stets, nachdem ein Event auf dem Element ausgelöst wurde. Für input, textarea und select wird dabei standardmäßig auf change, bei einer form auf submit und für alle anderen Elemente auf click reagiert.

Wollen wir diesen Auslöser ändern, können wir das Attribut hx-trigger, siehe Listing 2, nutzen. In diesem Fall wird htmx nur dann eine Anfrage erzeugen, wenn gleichzeitig auf den Button und die Shift-Taste gedrückt wird, da wir neben der Definition, das click-Event zu verarbeiten, einen Filter mit shiftKey in eckigen Klammern angegeben haben. Zusätzlich haben wir mit once und delay:1s noch zwei Event Modifier angegeben. Diese führen hier dazu, dass die Aktion nur einmal ausgeführt wird und die Anfragen an den Server erst eine Sekunde nach dem Klick ausgeführt werden. Die Dokumentation von hx-trigger zeigt neben allen Events und Modifiers auch noch die Möglichkeit von Polling. Listing 2 enthält zudem noch das Attribut hx-target. Durch dieses können wir angeben, welches Element auf unserer Seite durch das Ergebnis des HTTP-Aufrufs ersetzt werden soll, anstatt das Element zu ersetzen, welches die Anfrage ausgelöst hat.

...
<div id="employees">
</div>
<button hx-get="/employees"
        hx-trigger="click[shiftKey] once delay:1s"
        hx-target="#employees">
    Show employees
</button>
...
Listing 2: Komplexerer htmx-Ajax-Request

Das vierte wichtige Attribut für Ajax ist hx-swap. Dieses ermöglicht uns, eine Strategie für die Ersetzung anzugeben. Standardmäßig wird hierfür innerHTML genutzt und somit der Inhalt des ausgewählten Elements ersetzt. Wollen wir aber beispielsweise eine auf der Seite vorhandene Liste um die Elemente aus der Antwort erweitern, können wir, siehe Listing 3, beforeend nutzen. Es gibt für hx-swap noch eine Reihe von weiteren Ersetzungsmöglichkeiten und Modifikationen. Beispielsweise kann mit dem Modifier scroll:top dafür gesorgt werden, dass der Browser nach der Ersetzung zum neuen Inhalt scrollt und diesen am oberen Rand anzeigt.

...
<ul id="employees">
</ul>
<form hx-post="/employees"
        hx-target="#employees"
        hx-swap="beforeend">
    <label>Name <input type="text" name="name"></label><br>
    <button type="submit">Hinzufügen</button>
</form>
...
Listing 3: Angepasste Strategie für die Ersetzung

Neben den eingebauten Möglichkeiten gibt es für das Ersetzen auch die Möglichkeit, mittels Erweiterungen sogenannte Morph Swaps durchzuführen. Hierbei wird der neue Inhalt nicht nur eingefügt, sondern es werden nur geänderte Elemente aus der Antwort hinzugefügt. Das hat vor allem den Vorteil, dass der Browser Dinge wie den aktuellen Fokus nicht verliert und dass die Operation unter Umständen performanter abläuft, da der Browser weniger Dinge erneut zeichnen muss.

Zusätzlich erlaubt es uns htmx noch, mittels Out of Band Swapping auch Elemente durch mehrere verschiedene Elemente zu ersetzen. Hierzu muss in der Antwort auf eine htmx-Anfrage an einem mit id versehenen Element das Attribut hx-swap-oob auf true gesetzt werden. Anschließend wird htmx unabhängig vom angegebenen hx-target das aktuelle Element mit derselben id zusätzlich durch das aus der Antwort ersetzen.

Listing 3 zeigt, dass htmx zusätzlich zu dem angegebenen URI auch weitere Parameter mit an den Server schickt. Im Fall eines form mit POST werden dazu, wie bei einem regulären Absenden des Formulars, die input-Elemente als Body übertragen. Mithilfe der beiden Attribute hx-include und hx-params lässt sich aber auch hier das Verhalten noch weiter anpassen.

Ohne weitere Anpassungen antwortet der Server auf die Anfragen stets mit der gesamten Seite, da er nicht wissen kann, dass es sich um eine htmx-Anfrage handelt. Häufig brauchen wir aber nur einen kleinen Ausschnitt aus der Antwort für unsere Ersetzung. Zur Lösung dieses Problems gibt es zwei Wege. Wir können entweder für das Element, welches die Anfrage auslöst, mittels hx-select-Attribut einen CSS-Selektor angeben. Aus der Antwort wird dann vor der Ersetzung der Teil, der durch den Selektor beschrieben wurde, ausgeschnitten.

Die andere Möglichkeit besteht darin, bereits auf dem Server bei der Erzeugung des HTMLs darauf zu reagieren. Damit der Server dann doch erkennen kann, welche Anfragen von htmx kommen, wird dort eine Anzahl von HTTP-Headern spezifiziert, die bei Anfragen von htmx zusätzlich übermittelt werden. Der Server kann diese anschließend auswerten und dementsprechend andere Antworten liefern. Zudem steht auch eine Menge an HTTP-Headern für die Antwort zur Verfügung, mit denen der Server erweitertes Verhalten der Clientseite steuern kann.

Server-Sent Events und WebSockets

Neben HTTP-Anfragen via Ajax gibt es für htmx zwei Erweiterungen, um die beiden anderen Arten der Kommunikation, Server-Sent Events (SSE) und WebSockets, zwischen Browser und Server nutzen zu können. SSE ist dabei ein auf HTTP basierender Mechanismus, über den der Server dem Client Nachrichten schicken kann. Normalerweise benötigen wir hierzu eigenes JavaScript und können über eine EventSource auf die Nachrichten reagieren. htmx bietet uns hierfür die Möglichkeit, auf eigenes JavaScript zu verzichten und wie für Ajax diese Funktionalität deklarativ zu beschreiben.

Hierzu binden wir, wie in Listing 4 zu sehen, die SSE-Erweiterung ein und nutzen das Attribut sse-connect, um die Verbindung zum Server aufzubauen. Dabei wird, wie für Ajax, davon ausgegangen, dass der Server die Nachrichten als HTML schickt. Im Beispiel wird nun immer, wenn eine neue Nachricht vom Server an den Browser geschickt wird, diese an die vorhandene Liste angehängt, ohne dass eine Interaktion stattfinden muss. Dafür nutzen wir auch hier das bereits aus Ajax bekannte hx-swap-Attribut.

...
<h1>Server Sent Events</h1>
<div hx-ext="sse" sse-connect="/sse/messages">
    <ul sse-swap="message" hx-swap="beforeend">
    </ul>
</div>
...
Listing 4: Erweiterung um Server-Sent Events

Benötigen wir zusätzlich auch noch die Möglichkeit, Nachrichten vom Client an den Server zu senden, können WebSockets verwendet werden. Auch hier gibt es mit der WebSockets-Erweiterung die Möglichkeit, die bereits bekannten htmx-Mechanismen einzusetzen. Listing 5 zeigt den Einsatz dieser Erweiterung.

...
<h1>Web Sockets</h1>
<div hx-ext="ws" ws-connect="/ws/messages">
    <ul id="messages">
    </ul>
    <form id="form" ws-send>
        <input name="message" autofocus>
    </form>
</div>
...
Listing 5: WebSockets-Erweiterung

Neu ist hierbei vor allem das Attribut ws-send, über das wir den Inhalt der Nachrichten bestimmen können. In diesem Fall sendet der Browser diese in Form von JSON über den WebSocket-Kanal an den Server. Neben der eigentlichen Nachricht werden, wie in Listing 6 zu sehen, auch die bereits in Ajax gesehenen Metadaten mit an den Server übermittelt, damit dieser bei Bedarf spezifisch antworten kann.

{
    "message": "Michael",
    "HEADERS": {
        "HX-Request": "true",
        "HX-Trigger": "form",
        "HX-Trigger-Name": null,
        "HX-Target": "form",
        "HX-Current-URL": "http://localhost:8080/ws"
    }
}
Listing 6: Payload einer WebSocket-Nachricht an den Server

Spring- und Thymeleaf-Integration

Da htmx im Kern daraus besteht, HTML-Elemente um eigene Attribute zu erweitern, funktioniert es in Kombination mit den meisten serverseitigen Template-Engines ohne spezifische Integrationslogik. Bei der Nutzung von Thymeleaf gibt es jedoch die Besonderheit, dass Ausdrücke nur in bekannten Attributen ausgewertet werden. Wir könnten nun die hx-*-Attribute mittels th:attr erzeugen. Wie Listing 7 zeigt, ist der resultierende Code jedoch etwas schwer zu lesen. Deswegen gibt es in Kombinaten mit Spring im htmx-spring-boot-Projekt einen spezifischen Dialekt für htmx. Dieser erlaubt es uns, htmx-Attribute mittels hx:* zu definieren. Diese Art, siehe Listing 8, ist deutlich lesbarer.

...
<ul id="employees">
</ul>
<form th:attr="hx-post=@{${#mvc.url...}}, hx-target='#employees',...">
    <label>Name <input type="text" name="name"></label><br>
    <button type="submit">Hinzufügen</button>
</form>
...
Listing 7: htmx mit Thymeleaf nutzen
...
<ul id="employees">
</ul>
<form hx:post="@{${#mvc.url...}}" hx:target="'#employees'" ...>
    <label>Name <input type="text" name="name"></label><br>
    <button type="submit">Hinzufügen</button>
</form>
...
Listing 8: htmx-Thymeleaf-Dialekt

Neben diesem Dialekt enthält das Projekt auch noch eine Autokonfiguration für Spring Boot. Diese ermöglicht es uns, auf Serverseite spezifisch auf von htmx ausgelöste Anfragen zu antworten. Hierfür, siehe Listing 9, stehen uns sowohl Annotationen, wie @HxRequest für das Mapping von Anfragen auf Methoden, als auch Klassen, wie HtmxResponse, um komplexere Antworten inklusive der von htmx unterstützten Response-Header zu erzeugen, zur Verfügung. Es ist auch zu sehen, wie wir über mehrere Views und Thymeleaf-Selektoren wie employees/index :: #employees eine Antwort mit Out of Band Swaps erzeugen können und dadurch mehrere Bereiche der Seite dynamisch ausgetauscht werden.

...
@HxRequest
@GetMapping
public HtmxResponse employeesList() {
    var employees = ...;

    return HtmxResponse.builder()
            .view(new ModelAndView("employees/index :: #employees")
                .addObject("employees", employees))
            .view(new ModelAndView("employees/index :: #form")
                .addObject("newEmployeeForm", new NewEmployeeForm()))
            .build();
}
...
Listing 9: htmx-Antworten in Spring Boot erzeugen

Die Readme des Projekts zeigt außerdem, wie die HtmxResonse für die Behandlung von Fehlern innerhalb eines @ExceptionHandler genutzt werden kann. Zudem gibt es auch ein wenig Unterstützung für Spring Security in Form eines HxRefreshHeaderAuthenticationEntryPoint. Konfigurieren wir diesen, verhindern wir, dass im Falle eines Fehlers die Login-Seite anstelle des korrekten Inhalts für die Ersetzung verwendet wird.

Derzeit enthält die Bibliothek noch keinen Support für die beiden Erweiterungen um Server-Sent Events oder WebSockets. Alles in allem ist das Projekt aber zu empfehlen, denn es stellt doch eine Erleichterung dar.

Fazit

In diesem Artikel haben wir mit htmx eine Bibliothek kennengelernt, deren Ziel nicht weniger als die Ergänzung von, aus Sicht der Maintainer, fehlenden Funktionalitäten von HTML ist. Dabei setzt die Bibliothek konsequent auf eine deklarative Auszeichnung, mit eigenen Attributen an den existierenden HTML-Elementen, und auf vom Server generiertes HTML.

Schränkt man sich in der Nutzung ein wenig ein und verwendet primär die Boost-Funktion oder sorgt dafür, die Ajax-Funktionalitäten nur in Verbindung mit Hyperlinks und Formularen einzusetzen, kann htmx dazu beitragen, eine durch HTML getriebene Anwendung mit funktionierendem Progressive Enhancement zu erstellen. Hierzu ist aber dann auch auf Serverseite ein gewisser Buy-in vonnöten, da hier die Auswertung der von htmx gesendeten HTTP-Header bei Anfragen sinnvoll ist.

Möchten wir eine serverseitig HTML generierende Anwendung, ohne SPA, bauen, ist neben htmx allerdings auch Hotwire ein Kandidat. Der Artikel „Progressive Enhancement mit Hotwire“ meines Kollegen Joachim Praetorius bietet hierzu einen schönen Überblick und Einstieg in das Thema.

Ich persönlich halte htmx, vor allem in Verbindung mit Progressive Enhancement, für eine gute Idee, sehe aber auch die Gefahr, dass der deklarative Ansatz ab einer bestimmten Größe an seine Grenzen stoßen kann. Denn den Überblick über alle Selektoren und erwarteten Antworten zu behalten, kann durchaus eine Herausforderung sein. In dieselbe Kerbe schlägt auch der Artikel „HTMX: Die perfekte UI-Technologie?“ von Golo Roden.

Letztlich muss dann, wie immer, doch jeder für sich selbst im Rahmen des im eigenen Projekt vorhandenen Kontextes entscheiden, ob htmx eine passende Wahl ist oder nicht.