Hotwire can be used just fine with Spring Boot out of the box. However, as a couple of examples have shown, it usually requires a bit of boilerplate code to be written on the server side. This library attempts to remove that boilerplate code by providing a few Spring MVC extensions that make it easy to use Thymeleaf templates and template fragments as Turbo Streams.
-
Conveniently bundles the Hotwire WebJAR.
-
API to logically define Turbo Streams in WebMVC controllers.
-
Spring Boot auto configuration to set up Spring MVC to automatically render
TurboStreams
instances as Thymeleaf templates and fragments. -
A
Hotwire
instance available as bean or controller method argument to actively renderTurboStreams
instances in SSE compatible format.
An example project can be found here.
Add the library to your Spring MVC application:
<dependencies>
<dependency>
<groupId>de.odrotbohm.playground</groupId>
<artifactId>hotwire-spring-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependency>
</dependencies>
<!-- For snapshot access -->
<repositories>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>
If you haven’t already, please make sure you also have the Spring Boot web starter included.
@PostMapping(path = "/", produces = Hotwire.TURBO_STREAM_VALUE)
TurboStreams indexStream(Model model) {
// times is a List<Long> System.currentTimeMillis() as elements.
model.addAttribute("times", Arrays.asList(now()));
return new TurboStreams()
.append("pings").with("index :: ping");
}
Note, how we return an instance of TurboStreams
instance that allows us to logically define which streams we want to set up.
In this particular case we define exactly one stream but could accumulate more as well to update other parts of the page.
Read more on Turbo Streams in general here.
Spring MVC will render those in the format expected resolving the given Thymeleaf template or template fragment reference as actual payload.
I.e. a template snippet like this:
<div>
<p>Ping times</p>
<ol id="pings">
<li data-th-fragment="ping" data-th-each="time : ${times}">[[${time}]]</li>
</ol>
</div>
the response would look like this:
<turbo-stream action="append" target="pings">
<template>
<li>123456789</li>
</template>
</turbo-stream>
The primary support for streaming events consists of API to easily produce the representations expected by Hotwire.
@Controller
@RequiredArgsConstructor
class TurboStreamsSseController {
private final HotwireEvents events; (1)
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter indexSse() {
return events.initStream(); (2)
}
@Scheduled(fixedRate = 2000)
void pushEvent() throws IOException {
Map<String, Object> model = new HashMap<>();
model.put("time", System.currentTimeMillis());
TurboStreams streams = new TurboStreams()
.replace("load").with("index :: load"); (3)
events.push(streams, model); (4)
}
}
-
Inject
HotwireEvents
. This is a prototype scope bean available in the application context. -
Initialize an event stream. They can be named explicitly in case a controller wants to produce multiple ones.
-
Use of the
TurboStreams
API to define the streams to be sent to the client. -
Push the
TurboStreams
to the stream (here: the one with the default name).
-
Dedicated WebFlux support?
-
Improve the translation of
TurboStreams
into aView
. We currently use aHandlerInterceptor
but that has to guess about the case it is supposed to kick in quite a bit. AHandlerMethodReturnValueHandler
looks like a better alternative. However, decorating the existing list of them is rather cumbersome: post process allRequestMappingHandlerAdapters
, wrap the ones contained in the adapter in aHandlerMethodReturnValueHandlerComposite
, decorate that and set that back on theRMHA
. -
Anything else? Please file an issue!
-
A blog post by Josh Graham (sample code) focussing on the use within WebFlux and Kotlin.
-
Hotwire Demo — by Joachim Praetorius and Tobias Erdle of INNOQ