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:
@Controller
public 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:
hx-trigger="click"
tells HTMX to initiate Ajax on a click.hx-target="closest li"
tells HTMX where to put the response from the Ajax request. In our case, we want to replace the nearest list-item element. (Remember that ourdelete
endpoint returns the whole list-item markup for the item.)hx-swap="outerHTML"
tells HTMX how to swap in the new content, in this case, replacing the whole element.th:hx-post="|/todos/${item.id}/complete|"
tells HTMX that this is an active Ajax element that issues aPOST
request to the specified URL (ourto completeTodo
endpoint).
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 span
s:
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.