Skip to content

Latest commit

 

History

History

hotwire-spring-boot

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Spring Boot Hotwire integration

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.

Features

  • 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 render TurboStreams instances in SSE compatible format.

An example project can be found here.

Quickstart

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>

Server-Sent Events support

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)
  }
}
  1. Inject HotwireEvents. This is a prototype scope bean available in the application context.

  2. Initialize an event stream. They can be named explicitly in case a controller wants to produce multiple ones.

  3. Use of the TurboStreams API to define the streams to be sent to the client.

  4. Push the TurboStreams to the stream (here: the one with the default name).

Still open / ideas

  • Dedicated WebFlux support?

  • Improve the translation of TurboStreams into a View. We currently use a HandlerInterceptor but that has to guess about the case it is supposed to kick in quite a bit. A HandlerMethodReturnValueHandler looks like a better alternative. However, decorating the existing list of them is rather cumbersome: post process all RequestMappingHandlerAdapters, wrap the ones contained in the adapter in a HandlerMethodReturnValueHandlerComposite, decorate that and set that back on the RMHA.

  • Anything else? Please file an issue!

Contributors and references