This post is part of a series comparing different ways to implement asynchronous requests on the client to augment the latter. So far, I described the process with Vue.js and Alpine.js. Both are similar from the developers' point of view: they involve JavaScript.
In this post, I'll focus on HTMX, whose approach is quite different.
I'll follow the same structure as in the previous posts of the series. Here's the setup, server- and client-side.
Here is how I integrate Thymeleaf and HTMX in the POM:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <!--1-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <!--1-->
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId> <!--1-->
<version>0.52</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId> <!--2-->
<version>2.0.1</version>
</dependency>
</dependencies>
The code on the HTML side is straightforward:
<script th:src="@{/webjars/htmx.org/dist/htmx.js}" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script> <!--1-->
We want to implement the same features as previously.
HTMX implements a radical approach that is different from traditional AJAX frameworks. They force you to develop an HTTP API that accepts and returns JSON. With HTMX, you return HTML fragments instead. HTMX uses it to replace the DOM elements that you configured. Hence, you need to write neither JavaScript nor deal with JSON and serialization of entities.
HTMX nicely complements Thymeleaf because both work with page fragments. We can align Thymeleaf's fragments to HTMX's responses. It requires thinking ahead, which differs from the previous AJAX/API/JSON standard, but it's worth it.
Let's list interactions and what fragment we replace for each of them:
Here's the conceptual fragments design for our app:
-------------------- APP --------------------
| index.html |
| |
| ---------------- TABLE ---------------- |
| | table.html | |
| | | |
| | ------------- LINES ------------- | |
| | | lines.html | | |
| | | | | |
| | --------------------------------- | |
| --------------------------------------- |
---------------------------------------------
I'll split the HTML page into these fragments. Because we render them via Thymeleaf, we can split each into their dedicated file for a cleaner separation. At initial load time, we use Thymeleaf's replace
directive; we use HTMX for asynchronous client-side interactions.
We will start with the cleanup feature, as it's the easiest one with HTMX.
Here's the HTML code:
<tbody id="lines">...</tbody> <!--1-->
<button class="btn btn-warning"
hx-trigger="click" <!--2-->
hx-delete="/htmx/todo:cleanup" <!--3-->
hx-target="#lines"> <!--4-->
Cleanup
</button>
lines
DOM elementclick
eventDELETE
HTTP request to the URLlines
DOM element with it
Note that there's no explicit JavaScript involved, not a single line of code. HTMX takes care of it.
On the server side, the code is the following:
fun htmx(todos: MutableList<Todo>) = router {
DELETE("/htmx/todo:cleanup") {
todos.removeIf { it.completed } //1
ok().render("htmx/lines", mapOf("todos" to todos)) //2
}
}
render()
function, instead of body()
for API calls. Because of our previous file split, we can render only the needed HTML fragment. It uses Thymeleaf for any necessary server-side rendering.
That's the heart of HTMX: bind an HTTP call to a client-side event, and replace the configured DOM element with the server response.
Adding a new todo follows the same principle, but the DOM element is the whole table to reset the label
value. If interested in the complete, look at the code.
While I mentioned that we will not return anything from the check request, it presents an exciting challenge. That's the reason why I am only addressing it now.
We have two challenges when clicking on the checkbox:
todo
HTMX offers the hx-vals
for the JSON payload. However, the URL is different for each row as we want to include the ID in the path. We must generate it server-side with Thymeleaf. TIL: Thymeleaf can manage any HTML attribute prefixed with th:
: it will process the value as usual and write the attribute's name unprefixed.
<input type="checkbox"
th:checked="${todo.completed}" <!--1-->
hx-trigger="click" <!--2-->
th:hx-patch="'/htmx/todo/' + ${todo.id}" <!--3-->
hx-vals='js:{"checked": event.target.checked}' /> <!--4-->
todo
is completedclick
eventsPATCH
request to the server, with Thymeleaf having replaced the id
with the value in the HTML previously
Note that, as explained above, I ignored the response. In a real-world scenario, you should check/uncheck the checkbox depending on the value returned to avoid keeping the server state and the UI in synch.
In the two previous posts, I described Vue and Alpine. We configured Spring Boot to return JSON. With HTMX, we configured it to return HTML. Additionally, we didn't need any JavaScript code to send the requests from the client.
Icing on the cake, there's a great synergy between Thymeleaf and HTMX: we can split the page into fragments and reuse them on both sides.
The complete source code for this post can be found on GitHub:
To go further: