Dieser Blogpost ist auch auf Deutsch verfügbar
Rendering websites on the server and then using “progressive enhancement” to ensure that they function properly without full reloads, as many users expect: nowadays, that feels like a niche topic.
Hotwire is a new collection of tools that make it easier to achieve good results with this strategy. For “classic” web applications, it provides us with a series of best practices that make a comprehensive SPA framework superfluous.
“We have a backend with REST-based JSON interfaces and then an Angular application on top”
For many of us, a description like this has become a commonplace when referring to any web application built in recent years. The reasons for this separation often tend towards “that’s how you do it now” or “that’s what our team has experience with”. It is rare for a requirement to necessitate the use of a common SPA framework. [Elsewhere], (https://www.innoq.com/de/articles/2019/04/wider-die-spa-fixierung/) we have already discussed in detail what disadvantages this “modern” division entails.
At the end of last year, the Basecamp team introduced the Hotwire Toolkit. Based on their experiences of building Basecamp and Hey, they published a very manageable quantity of JavaScript modules as open source, to produce the behavior of an SPA without forgoing a solid SSR foundation.
You can find all of the coding examples that I use in this article in our GitHub repository. That is where we show you a working integration of Hotwire in a plain vanilla Spring boot application.
Batteries included
Hotwire is synonymous with the entire approach: sending HTML Over the Wire, using native Web formats to make the application as swift and reactive as possible for users, and as intuitive and consistent as possible for developers.
Three modules are in place to help Hotwire reach this target vision:
- Turbo – replaces navigation in classic web applications to enable behavior analogous to SPAs: page transitions without reloads, splitting pages into components, updates in other parts of a page, and streaming updates. These 89kB of JavaScript deal with the heavy lifting and replace a large part of the tasks otherwise performed by Angular, React, Vue and the like.
-
Stimulus – uses
data
attributes in HTML markup to permit the generation of controllers that provide the functionality of a custom element – without actually building a custom element. Controllers of this type also deal with the management of state and data. - Strada – will provide a method for integrating native code and WebViews in mobile apps. Strada has been announced but no code has been published yet.
The important point to note is that the three modules combine wonderfully with one another, but are not mutual prerequisites.
We will look at what each module provides in more detail in the following sections.
Turbo
As mentioned above, Turbo provides a whole raft of functions that build on one another. And although our Example Repository shows the integration in Spring, the majority of the functionality is completely independent of the framework, as it is only achieved through corresponding attributes in HTML.
SPA-style navigation with Turbo Drive
Seamless navigation, as seen in SPAs, is the easiest to implement: no reload should be visible for the user and the transition between pages remains seamless.
is all that is required – the import loads Turbo and starts a session (alternatively, you can include Turbo from a CDN like unpkg or skypack).
Turbo then takes over all link clicks and form submissions that remain within the same origin (i.e. point to the same protocol/host name/port combination).
These are executed in the background as XHR and the browser history is adjusted (to ensure that Back and Forward function correctly).
When the response arrives, the page content displayed at that moment in the browser is adjusted. Elements from the <head>
are merged, while those in the <body>
are replaced by the new ones.
If possible, Turbo Drive already displays a version of the page from the cache, which is then updated with the current result (stale-while-revalidate), leading to the provision of an even faster result for the user.
Further data
attributes for elements on the page can be used for more detailed control of the behavior of Turbo Drive. This ensures that Turbo is working exactly as intended – normally, the default is itself very good. The handbook provides a detailed explanation of the individual options.
Subdividing a page into blocks with Turbo Frames
Although Turbo Drive moves the page update into the background, you may sometimes want to control which parts of a page an interaction refers to.
As an example, let’s take a wiki page where you can edit each individual section.
The section that we want to isolate on the page is wrapped in an <turbo-frame>
element, and can therefore be addressed individually. Turbo automatically ensures that interactions between these “frames” really do only refer to them. After receiving the response, only the part of the page within the <turbo-frame>
is updated (as in an SPA, but without additional change detection in the virtual DOM).
If the response to POST /sections/1/edit
also contains a <turbo-frame>
with the same id
, this is just extracted from the response and replaces the <turbo-frame>
for the original page.
This way, our example allows us to transparently replace the section on our wiki page with an editing form.
It does not matter whether the response only contains the <turbo-frame>
fragment that triggered the request, or a complete page.
However, if you always send the complete page, you’ll directly build a “progressively enhanced” version of your page, as it continues to work without JavaScript being present.
that looks great. Thanks for the example @__jpr. I was hoping someone would try it out in Java land. The best part: even if you forget to do the `npm install` to deliver the JS assets (as I did ;)), the app is still working. I really like this progressive enhancement thing... pic.twitter.com/HI31zUBjBj
— Mario David (@mariomddavid) January 18, 2021
We‘d love to show you a tweet right here. To do that, we need your consent to load third party content from twitter.com
Lazy Loading with Turbo Frames
Lazy loading for page components is one of the things that you get along with Turbo Frames, “just like that”. When loading a page, you can deliver individual sections just as empty fragments with placeholders, negating the need to fetch data (which may be slow) and providing an even faster result for the user.
The “src” attribute for the <turbo-frame>
informs Turbo, that it is to trigger a request automatically and where to fetch the content from.
The response is handled in the same way as a request that the user triggers: the relevant <turbo-frame>
is extracted and replaces the static markup.
Update other page areas
The example snippet above has another role: it triggers an HTTP request (which takes place in the background as usual), but applies the result to a different area of the page (the <turbo-frame>
with the id
comments
). This is controlled by the ‘data-turbo-frame’ attribute, which informs Turbo which frame is to be addressed.
Dynamic updates with Turbo Streams
Turbo Streams is certainly the topic that has generated the most interest. Quite understandably so, as it was introduced with ‘WebSockets Live Updates for a Website’. It is indeed possible to connect WebSockets (or other Event Streams), but we will come back to that later. Firstly, we want to take a look at what Turbo Streams actually does.
Turbo Streams permits multiple parallel actions for a website to be sent in one response. To do so, it defines a specific format for the response:
Each object is enclosed by a <turbo-stream>
element that specifies the action to be executed in action
. The target
attribute carries the ID for the addressed element. This does not have to be a Turbo Frame or anything like that, but can be any HTML element with the corresponding id
attribute (in the same way as you would use it in document.querySelector()
).
Possible actions are restricted to the five specified above (append
,prepend
,replace
,update
and remove
). Taking a look at the implementation reveals that these are the ones that map easily to the operations for a node in the DOM API. Once again, existing functionality of the web platform is used to prevent inventing something specifically for Turbo. (And, to be honest, these actions are sufficient for plenty of cases.)
You take the actual content that you want to insert and put it into a <template>
element to allow easy handling in the DOM. During processing, this element is added to the DOM, the content is used and then the <template>
element is removed again.
You can put as many of these <turbo-stream>
elements as you want into your response, assign the content type text/vnd.turbo-stream.html
to the lot, and that’s it, the functionality is done.
Turbo checks the content type for “normal” responses (resulting from form submits, for example) and applies the Turbo Streams updates instead of the normal merging logic, if the content type for the response is text/vnd.turbo-stream.html
.
Alternatively, event streams can be used to transmit these updates. The advantage here is that user interaction is not necessarily required to get the updates. Event streams also represent an open connection between browser and server, enabling continuous updates (for a chat or a live ticker, for example).
As you register a stream of this type explicitly with the JavaScript API for Turbo, it is no longer necessary to set the content type. Turbo relies on the ability to interpret individual events as Turbo Stream actions (if this is not the case, you will be informed about this on the console).
Event Stream sources can either be server-sent events or WebSockets, which are each instantiated with their specific class in the browser:
Handling these connections on the Java side does not need any Turbo-specific code, they are just general SSE or WebSocket connections. The only thing that matters is that the response format corresponds to the one I’ve outlined above.
Stimulus
The concept behind Stimulus is the transformation of elements of an existing website into controllers
.
These can react to events that occur within their scope, enabling any number of complex interactions on an HTML page.
A controller
also has references to all elements that it contains and can exert control over these. This allows retroactive adjustment of the existing markup. An example of this would be the addition of an option to a text field that copies the text within it to the clipboard (including the necessary buttons).
Using Stimulus allows you to do so without writing a custom element by yourself – just by adding a couple of attributes to your existing markup. This procedure again reflects the thinking of progressive enhancement, as you must already have the basis for your elements in the normal HTML code before you can extend these with the controller.
Implementation takes place in an ES6 JavaScript class, which extends the base class Controller
contained in Stimulus.
Here you simply write any number of functions that you need to fulfill the functionality you want your Controller to have. No Turbo speficic code is necessary.
The additional benefit of deriving from controller
lies in the two static fields values
and targets
.
Stimulus automatically creates bindings to data
attributes in HTML for these, allowing them to be addressed directly in the code without your own boilerplate coding. The documentation provides details about the usage and the supported file formats.
In the same way as for custom elements, you also can receive lifecycle events. You can implement methods for initialize
, connect
and deconnect
, and use them to perform the required setup and teardown.
If you do not use webpack as bundler, you must now provide a small piece of code to connect the controller implementation with a logical name (in the same way as for the registration of a custom element):
On the website, you now complete the binding: using data-controller
attributes to specify which controller is to be bound to the element (the recently registered names are used):
The various data-connect-websocket
attributes follow the Stimulus naming convention and thereby permit the binding described above to the static
fields for the controller, which Stimulus does automatically.
Finally, data-action="click->connect-websocket#toggleStream"
binds a click handler to the <button>
and ensures that this calls up the toggle
method for our ConnectWebsocketController
class.
Embedding in Spring
As mentioned at the outset, we have simple embedding of all the functionality shown above in Spring Boot/Spring Web MVC published on GitHub. To be honest though, we have to say that most implementations are not really Spring-specific – in the end, it is about getting the right attributes or additional elements in your own HTML templates. Therefore, our templates contain most Hotwire-specific things, which could easily be transferred to a different templating library (we used Thymeleaf) or another Java framework (such as quarkus).
Content negotiation was the area requiring the most special effort, ensuring that you can return the name of a template as expected within the Spring @Controller
, if you want to return a Turbo Streams update from a normal method.
In this case the ViewResolver should return a View that gets created with the right Turbo-specific Content-Type. The snag with this is that the Turbo Library always sends Accept: text/vnd.turbo-stream.html
, but we only want to activate the Turbo Streams content type for specific responses.
Currently, our solution is a ViewResolver, that is configured with a hard list of the views it should handle.
If you prefer to use Webflux for Spring, we recommend the repository from Josh, which shows suitable implementation and describes this in quite some detail, too.
Conclusion
Hotwire feels a bit like the introduction of SSDs. Essentially, nothing really new is happening, but suddenly the time-honored method seems so simple, fast and obvious that you ask yourself why we ever searched for different answers.
Templating is centralized in one location, there are no endless battles with webpack or with a module configuration, no waiting 30 seconds for a bundle to be rebuilt and then 10 MB to be reloaded in the browser.
Without any configuration, the functionality does exactly what you expect, but still has enough options to let you make flexible adjustments. And all that is in declarative markup, with no need to dive into the depths of nested JS APIs. You can sense that the people involved in Hotwire know their way around web applications, and the trials and tribulations that they entail.
Turbo in particular is a rounded library (and lightweight too, when you consider what it can do) with step-by-step enabling of new features that give you the SPA feeling, without having to build an SPA.
Just as a response to normal form submits, Turbo Streams is a fantastic feature. In conjunction with event streams, it even feels a bit like magic. You can stream updates to the page “just like that”, without having to write huge chunks of code.
Stimulus does a good job of simulating custom elements without you having to commit to these yet. The target and value bindings also provide handy methods to define configurations declaratively in HTML instead of working with additional scripts. You feel that the focus really is HTML-first, reinforcing the idea of progressive enhancements.
Personally, I find Stimulus to be a good enhancement, but I prefer to use custom elements for the same functionality. That is simply because we then build directly on the web platform and not on a definition from Basecamp. There are already plenty of libraries that ease building Custom Elements for you, but we will approach that topic in another post.
In any case, it is worth taking a close look at Hotwire (whichever parts of it you want to use) and my advice to anyone starting work on something new is to try it out, and see if you really do miss anything from those big JavaScript frameworks.