Getting Started with Hotwire in a Java Webapp
Jan 12, 2021, 5:19 PM
Whenever I have a great deal of discretion over how a web app is made these days, I like to push to see how simple I can make the front end portion. I spend some of my client time writing heavy client-JS front ends in React and Angular and what-have-you, and, though I get why they are good, I kind of hate them all.
One of the manifestations of my desires has been this very blog, where I set out to try not only some interesting current tools on the Java side, but also challenged myself heavily to use little to no JavaScript. On that front, I was tremendously successful - and, in fact, the only JavaScript on here is the Turbolinks library, which intercepts same-app links and updates the changed parts inline, without the server knowing about the "partial refresh" going on.
Since then, Turbolinks merged with its cousin Stimulus and apotheosized into Hotwire, which is somewhere in between a JavaScript framework and a manifesto. Specifically, it's a manifesto to my liking, so I've been champing at the bit to use it more.
Hotwire Overview
The "Hotwire" name is a cheeky truncation of HTML-over-the-wire, which itself is a neologism for how the web has historically worked: your server sends HTML, and then your browser does stuff with that. It "needs" a new name to set it apart from full-JS apps, which amount to basically sending an application to the browser, having it initialize the app, and then having the app do what would otherwise be the server's job by way of shuttling JSON around.
Turbo is that part that subsumed Turbolinks, and it focuses on enhancing existing HTML and providing a few web components to bring single-page-application niceties to server-rendered apps. The "Drive" part is Turbolinks, so that was familiar to me. What interested me next was Turbo Frames.
Turbo Frames
If you've ever used the XPages Dojo Tab Container's partialRefresh
property before, Turbo Frames will be familiar. There are two main ways you can go about using it: making a "frame" that contains some navigable content (say, a form) that will then refresh in-place or making a lazy-loaded frame that pulls from another URL. The latter is what interested me now, and is what carries similar benefits to the Tab Container. It lets you serve the main page and then defer complex complication of an inner part without having to write your own JavaScript to do an API call or otherwise populate the section.
In my case, I wanted to do something very similar to the example. I have my main page, then a sidebar that can be potentially complicated to generate. So, I set up a Turbo Frame using this bit of JSP:
1 | <turbo-frame id="links" src="${pageContext.request.contextPath}/links"></turbo-frame> |
The only difference from the example, really, is the bit of EL in ${...}
, which just makes sure that the final URL adapts to wherever the app is hosted.
The "links" resource there is another MVC controller that renders a different JSP page, truncated like:
1 2 3 4 5 6 7 8 9 10 | <html> <head> <script type="text/javascript" src="${pageContext.request.contextPath}/webjars/hotwired__turbo/7.0.0-beta.2/dist/turbo.es5-umd.js"></script> </head> <body> <turbo-frame id="links"> <!-- expensive content here --> </turbo-frame> </body> </html> |
The <turbo-frame id="links">
on the initiating page matches up with the one in the embedded page to figure out what to extract and render.
One little side note here is my use of WebJars to bring in Turbo. This isn't an NPM-based project, so there's no package.json
bringing the dependency in, but I also didn't want to just paste the JS into my project. Fortunately, WebJars does yeoman's work: it makes various JS libraries available in Servlet-friendly Java JAR format, giving you a JAR with the JS from whatever the library is in META-INF/resources
. In turn, an at-least-reasonably-modern servlet container will serve files up from there as if they're part of your main app. That way, you can just use a Maven dependency and not have to worry.
A Hitch: 406 Not Acceptable
Edit 2021-01-13: Thanks to a new release of Turbo, this workaround is no longer needed.
When I first put this together, I saw that Turbo was doing its job of fetching from the remote URL, but it was getting a 406 Not Acceptable
response from the server. It took me a minute to figure out why - the URL was correct, it was just a normal GET request, and nothing immediately stood out as a problem in the headers.
It turned out that the trouble was in the Accept
header. To work with other Turbo components, Frames makes a request with a header like Accept: text/html; turbo-stream, text/html, application/xhtml+xml
. That first one - text/html; turbo-stream
- is problematic. I'm not sure if it's the presence of a qualifier at all on text/html
, the space, or the lack of an =
(as in text/html;charset=UTF-8
), but Liberty didn't like it.
Since I'm not (yet, at least) using Turbo Streams, I decided to filter this out on the server. Since MVC is built on JAX-RS, I wrote a JAX-RS request filter to find any Accept
values of this type and strip them out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Provider @PreMatching public class TurboStreamAcceptFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { MultivaluedMap<String, String> headers = requestContext.getHeaders(); if(headers.containsKey(HttpHeaders.ACCEPT)) { List<String> cleaned = headers.get(HttpHeaders.ACCEPT).stream() .map(accept -> { String[] vals = accept.split(",\\s*"); //$NON-NLS-1$ List<String> localClean = Arrays.stream(vals) .filter(val -> val.indexOf(';') < 0) .collect(Collectors.toList()); return String.join(", ", localClean); //$NON-NLS-1$ }) .collect(Collectors.toList()); headers.put(HttpHeaders.ACCEPT, cleaned); } } } |
Since those filters happen before almost anything else, this cleared up the trouble.
Summary
Setting the Accept
quirk aside, this was a pleasant success, and I look forward to using this more. I've found the modern Java stack of JAX-RS + CDI + MVC + simple JSP to be a delight, and Hotwire slots perfectly-smoothly into it. I still quire enjoy rendering HTML on the server and the associated perk of not having to duplicate business logic on both sides. Next time I have an app that requires a bit of actual JavaScript, I'll likely throw Stimulus into the mix here.