HTMX for Java with Spring Boot and Thymeleaf

By Matthew Tyson

Not long ago, we looked at how to build an HTMX application with JavaScript. HTMX also works with Java, so now we'll try that out using Spring Boot and Thymeleaf. This awesome stack gives you all the power and versatility of Java with Spring, combined with the ingenious simplicity of HTMX.

HTMX: A rising star

HTMX is a newer technology that takes plain old HTML and gives it extra powers like Ajax and DOM swaps. It’s included in my personal list of good ideas because it eliminates a whole realm of complexity from the typical web app. HTMX works by converting back and forth between JSON and HTML. Think of it as a kind of declarative Ajax.

Read an interview with HTMX creator Carson Gross.

Java, Spring, and Thymeleaf

On the other side of this equation is Java: one of the most mature and yet innovative server-side platforms bar none. Spring is an easy choice for adding a range of Java-based capabilities, including the well-designed Spring Boot Web project for handling endpoints and routing.

Thymeleaf is a complete server-side templating engine and the default for Spring Boot Web. When combined with HTMX, you have everything you need to build a full-stack web app without getting into a lot of JavaScript.

HTMX and Java example app

We’re going to build the canonical Todo app. It will look like this:

We list the existing to-do's, and allow for creating new ones, deleting them, and changing their completion status.

Overview

This is what the finished Todo app looks like on disk:

$ tree.├── build.gradle├── gradlew├── gradlew.bat├── settings.gradle└── src └── main ├── java │ └── com │ └── example │ └── iwjavaspringhtmx │ ├── DemoApplication.java │ ├── controller │ │ └── MyController.java │ └── model │ └── TodoItem.java └── resources ├── application.properties ├── static │ └── style.css └── templates ├── index.html ├── style.css └── todo.html

So, besides the typical Gradle stuff, the app has two main parts contained in the /src directory: The /main directory holds the Java code and /resources holds the properties file and two subdirectories with the CSS and Thymeleaf templates.

You can find the source for this project on its GitHub repo. To run it, go to the root and type $ gradle bootRun. You can then use the app at localhost:8080.

If you want to start the app from the ground up, you can begin with: $ spring init --dependencies=web,thymeleaf spring-htmx. That will install Thymeleaf and Spring Boot into a Gradle project.

The app is a normal Spring Boot application run by DemoApplication.java.

The Java Spring HTMX model class

Let’s begin by looking at our model class: com/example/iwjavaspringhtmx/TodoItem.java. This is the server-side model class that will represent a to-do. Here’s what it looks like:

public class TodoItem { private boolean completed; private String description; private Integer id; public TodoItem(Integer id, String description) { this.description = description; this.completed = false; this.id = id; } public void setCompleted(boolean completed) { this.completed = completed; } public boolean isCompleted() { return completed; } public String getDescription() { return description; } public Integer getId(){ return id; } public void setId(Integer id){ this.id = id; } @Override public String toString() { return id + " " + (completed ? "[COMPLETED] " : "[ ] ") + description; }}

This is a simple model class with getters and setters. Nothing fancy, which is just what we want.

The Java Spring HTMX controller class

On the server, the controller is the boss. It accepts requests, orchestrates the logic, and formulates the response. In our case, we need four endpoints used for listing the items, changing their completion status, adding items, and deleting them. Here's the controller class:

@Controllerpublic class MyController { private static List<TodoItem> items = new ArrayList(); static { TodoItem todo = new TodoItem(0,"Make the bed"); items.add(todo); todo = new TodoItem(1,"Buy a new hat"); items.add(todo); todo = new TodoItem(2,"Listen to the birds singing"); items.add(todo); } public MyController(){ } @GetMapping("/") public String items(Model model) { model.addAttribute("itemList", items); return "index"; } @PostMapping("/todos/{id}/complete") public String completeTodo(@PathVariable Integer id, Model model) { TodoItem item = null; for (TodoItem existingItem : items) { if (existingItem.getId().equals(id)) { item = existingItem; break; } } if (item != null) { item.setCompleted(!item.isCompleted()); } model.addAttribute("item",item); return "todo"; } @PostMapping("/todos") public String createTodo(Model model, @ModelAttribute TodoItem newTodo) { int nextId = items.stream().mapToInt(TodoItem::getId).max().orElse(0) + 1; newTodo.setId(nextId); items.add(newTodo); model.addAttribute("item", newTodo); return "todo"; } @DeleteMapping("/todos/{id}/delete") @ResponseBody public String deleteTodo(@PathVariable Integer id) { for (int i = 0; i < items.size(); i++) { TodoItem item = items.get(i); if (item.getId() == id) { items.remove(i); break; } } return ""; }}

You’ll notice that I’ve just created a static List to hold the items in memory. In real life, we would use an external data store.

For this tour, there are a few additional points of interest.

First, the endpoints are annotated with @GetMapping, @PostMapping, and @DeleteMapping. This is how you map Spring Web paths to handlers. Each annotation corresponds to its HTTP method (GET, POST, DELETE).

Spring Boot also makes it easy to grab parameters off the path using argument annotation @PathParameter. So, for the path /todos/{id}/delete, @PathVariable Integer id will contain the value in the {id} part of the path.

In the case of the createTodo() method, the argument annotated @ModelAttribute TodoItem newTodo, will automatically take the POST body and apply its values to the newTodo object. (This is a quick and easy way to turn a form submit into a Java object.)

Next, we use the item IDs to manipulate the list of items. This is standard REST API fare.

There are two ways to send a response. If the @ResponseBody annotation is present on the method (like it is for deleteTodo()) then whatever is returned will be sent verbatim. Otherwise, the return string will be interpreted as a Thymeleaf template path (you'll see those in a moment).

The Model model argument is special. It's used to add attributes to the scope that is handed off to Thymeleaf. We can interpret the following items method as saying: Given a GET request to the root/path, add the items variable to the scope as “itemList” and render a response using the “index” template.

@GetMapping("/") public String items(Model model) { model.addAttribute("itemList", items); return "index"; }

In cases where we're handling an AJAX request sent from the front end by HTMX, the response will be used by the HTMX component to update the UI. We’ll get a good look at this in practice soon.

The Thymeleaf templates

Now let’s have a look at Thymeleaf's index template. It lives in the /resources/templates/index.html file. Spring Boot maps the “index” string returned from the items() method to this file using conventions. Here's our index.html template:

<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <title>Items List</title> <script src="https://unpkg.com/htmx.org@1.9.12"></script> <link rel="stylesheet" href="style.css"> </head> <body> <h1>Stuff To Do</h1> <ul> <th:block th:each="item : ${itemList}"> <th:block th:replace="~\{'todo.html'}" th:args="${item}"></th:block> </th:block> </ul> <hr> <form hx-post="/todos" th:object="${newTodo}" hx-target="ul" hx-swap="beforeend"> <input type="text" name="description" placeholder="Add a new item..." required> <button type="submit">Add</button> </form> </body></html>

The basic idea in Thymeleaf is to take an HTML structure and use Java variables in it. (This is equivalent to using a template system like Pug.)

Thymeleaf uses HTML attributes or elements prefixed by th: to denote where it does its work. Remember when we mapped the root/path in the controller, we added the itemList variable to the scope. Here, we are using that inside a th:block with a th:each attribute. The th:each attribute is the iterator mechanism in Thymeleaf. We use it to access the elements of itemList and expose each as a variable-named item: item : ${itemList}.

In each iteration of itemList, we hand off the rendering to another template. This kind of template reuse is key to avoiding code duplication. The line

<th:block th:replace="~\{'todo.html'}"th:args="${item}"></th:block>

tells Thymeleaf to render the todo.html template and provide the item as an argument.

We'll look at the todo template next, but first notice that we are using the same template back in the controller, in both completeTodo and createTodo, to provide the markup that we send back to HTMX during Ajax requests. Put another way, we are using the todo.html as part of both the initial list rendering and to send updates to the UI during Ajax requests. Reusing the Thymeleaf template keeps us DRY.

The todo template

Now here’s todo.html:

<li> <input type="checkbox" th:checked="${item.isCompleted}" hx-trigger="click" hx-target="closest li" hx-swap="outerHTML" th:hx-post="|/todos/${item.id}/complete|"> <span th:text="${item.description}" th:classappend="${item.isCompleted ? 'complete' : ''}"></span> <button type="button" th:hx-post="|/todos/${item.id}/delete|" hx-swap="outerHTML" hx-target="closest li">🗑</button></li>

You can see we are providing a list-item element and using a variable, item, to populate it with values. Here's where we get into some interesting work with both HTMX and Thymeleaf.

First, we use th:checked to apply the checked status of item.isComplete to the checkbox input.

When clicking the checkbox, we issue an Ajax request to the back-end using HTMX:

Something to note in using Thymeleaf with HTMX is that you end up with complex attribute prefixes, as you see with th:hx-post. Essentially, Thymeleaf runs first on the server (the th: prefix) and populates the ${item.id} interpolation, then hx-post works as normal on the client.

Next up, for the span, we just display the text from item.description. (Notice that Thymelef’s expression language lets us access fields without using the get prefix.) Also of note is how we apply the completed style class to the span element. Here is what our CSS will use to put the strike-through decoration on completed items:

th:classappend="${item.isCompleted ? 'complete' : ''}"

This Thymeleaf attribute makes it simple to conditionally apply a class based on boolean conditions like item.isComplete.

Our Delete button works similarly to the complete checkbox. We send the Ajax request to the URL using the Thymeleaf-supplied item.id, and when the response comes back, we update the list item. Remember that we sent back an empty string from deleteTodo(). The effect will therefore be to remove the list item from the DOM.

The CSS stylesheet

The CSS stylesheet is at src/main/resources/static/style.css and it's nothing remarkable. The only interesting bit is handling the strike-through decoration on the spans:

span { flex-grow: 1; font-size: 1rem; text-decoration: none; color: #333; opacity: 0.7;}span.complete { text-decoration: line-through; opacity: 1;}

Conclusion

The combination of HTMX, Java, Spring, and Thymeleaf opens up a world of possibilities for building fairly sophisticated interactions with a truly minimal amount of boilerplate code. We can do a huge amount of typical interactivity without ever writing JavaScript.

At first glance, the Java-HTMX stack seems like it finally delivers on the promise of Java-centric Ajax; something like what Google Web Toolkit once set out to do. But there's more. HTMX is an attempt to re-orient web applications to the true nature of REST, and this stack shows us the way. HTMX is server-side agnostic, so we can integrate it with our Java back-end without difficulty.

© Info World