Thymeleaf and HTMX - More Thoughts

23 Aug 2024

This is a follow-up to an earlier note Using Thymeleaf Fragments with HTMX.

There are certain Thymeleaf expressions which will only work correctly if they are executed in the context of a web application - and only if Thymeleaf has access to that web application’s web context.

What do I mean by “web context” here? Basically, I mean:

The context in which web components execute … an object that implements the ServletContext interface.

A servlet context is the glue between your Java web application’s logic and the container (e.g. Tomcat) in which that application runs. If you want to dive into the details, you can read the latest Jakarta servlet specification.

How is Thymeleaf affected by this?

Take, for example, Thymeleaf’s link URL syntax - for example:

th:href="@{/order/{orderId}/details(orderId=${o.id})}"

This is a “context-relative” URL (it begins with a /).

The context name (typically your web application’s base name) will be added to the URL by Thymeleaf.

Similarly, Thymeleaf has syntax for page-relative URLs, server-relative URLs and so on.

Thymeleaf can only evaluate these expressions if it has access to the relevant information from your web application.

Thymeleaf also has expressions which deal with request and session attributes, such as:

${param.foo}

${session.foo}

…and so on.

Now, Thymeleaf has different template resolvers which can be used to build your Thymeleaf template engine. Examples include:

…and others.

The classloader template resolver (the one I use in my earlier article and its related code) does not know anything about the web application in which it is running. When it locates and processes your Thymeleaf templates, it simply does not know how to process expressions such as these:

th:href="@{/order/{orderId}/details(orderId=${o.id})}"

${param.foo}  

${session.foo}

Now, it’s possible you may not even notice this, if you never use these expressions in your Thymeleaf templates.

For example, instead of using @{...}, you may use this:

th:href="'/order/' + ${orderId} + '/details'"

or this:

th:href="${'/order/' + orderId + '/details'}"

or this:

th:href="|/order/${orderId}/details|"

or some similar variation. And by doing so you have side-stepped the problem that your Thymeleaf classloader render does not know what the web site context is.

Turning our attention to a web framework such as Javalin, we may ask the following question:

How is my web-unaware template resolver able to execute @{...} expressions successfully when it is registered and run in my Javalin application?

And indeed it is true - when you register a Thymeleaf engine in Javalin, it just works for these context-dependent expressions. Why? Because Javalin provides the missing web context information for your Thymeleaf engine.

The problem regarding HTMX, as discussed in my earlier note about HTMX is that Javalin does not expose the required Thymeleaf method(s) needed to return a string of HTML - and that is what HTMX is typically expecting to receive when it makes an Ajax call.

We cannot define a Thymeleaf web application template resolver up-front for Javalin to use, because Javalin needs to register our Thymeleaf engine before Javalin has launched (and therefore before we have any web context).

We can work around this chicken-and-egg problem by imitating what Javalin does.

We can take our classloader template resolver (and the Javalin engine which uses that resolver) and we can provide the required web context on-the-fly for each HTMX request.

We start (just like Javalin does) by using Thymeleaf’s JakartaServletWebApplication class.

Together with this, we use the standard Javalin Context object to access the underlying web request and response objects.

With these we can build a Thymeleaf WebContext.

The code looks like this:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import io.javalin.http.Context;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.web.IWebExchange;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;

...

var application = JakartaServletWebApplication.buildApplication(ctx.req()
        .getServletContext());
var webExchange = application.buildExchange(ctx.req(), ctx.res());
var webContext = new WebContext(webExchange, webExchange.getLocale(), model);
String html = myTemplateEngine.process(templateName, fragmentNames, webContext);

Here the model object is just a standard Map<String, Object> containing the data we want to render into our template; and myTemplateEngine is my previously defined Thymeleaf engine (for example, like this one).

You don’t need to use fragment names.

The Thymeleaf process() method for our TemplateEngine has a variety of signatures, one of which only requires a template name (no fragments).

Once you have your String html (correctly rendered, including any @{...} expressions!), you can use Javalin’s ctx.html(...) to send the HTML string back to the HTMX element which originated the Ajax request.

So, yes, this does require you to re-build the Javalin webContext, for every such HTMX request - but that should be a negligble overhead.

The original point of the exercise was to simplify our client-side code, by using HTMX - and this allows us to achieve that goal, without compromising on the Thymeleaf expressions we are allowed to write.

That was a long discussion for basically four lines of code… But worth it, I hope.