Description
A handler method returning Flux<T>
where T
is "something that can render a template" would be quite useful, especially in the light of the popularity and ease of use of things like the @hotwired/turbo
and htmx.org JavaScript modules. Those client libraries both have support for "streams" of HTML elements coming from the server, which get transcluded into the "main" page on the client. They also both support SSE streams containing HTML data. It would be nice to be able to render in both styles.
Webflux and MVC currently have support for SSE. E.g. with Webflux you can return Flux<String>
or Flux<ServerSentEvent>
from a handler method, but in both cases you have to render the data yourself. It would be handy to be able to delegate the rendering to a template engine, so Rendering
(WebFlux) and ModelAndView
(MVC) seem like a good fit. Thymeleaf also has some native (if a bit clumsy) support via ReactiveDataDriverContextVariable
, so there is some prior art there. You could see this feature request as a generalization of that.
Simple example for Turbo on Webflux (for MVC just replace Rendering
with ModelAndView
) and SSE:
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Rendering> stream() {
return Flux.interval(Duration.ofSeconds(2)).map(value -> event(value));
}
private Rendering event(long value) {
return Rendering.view("stream").modelAttribute("value", value)
.modelAttribute("time", System.currentTimeMillis()).build();
}
with a template (e.g. in mustache but could be thymeleaf etc.):
<turbo-stream action="append" target="load">
<template>
<div>Index: {{value}} Time: {{time}}</div>
</temlate>
</turbo-stream>
The result would be an infinite stream, e.g.:
data: <turbo-stream action="append" target="load">
data: ...
data: <turbo-stream action="append" target="load">
data: ...
...
An example with HTMX and the HTML "stream" would be the same controller but with a different produces
media type:
@GetMapping(path = "/updates", produces="text/vnd.turbo-stream.html")
public Flux<Rendering> stream() {
return Flux.just(event("one"), event("two");
}
private Rendering event(String id) {
return Rendering.view("update").modelAttribute("id", id)
.modelAttribute("time", System.currentTimeMillis()).build();
}
with a template (e.g. in mustache but could be thymeleaf etc.):
<div htmx-swap-oob="true" id="{{id}}">
<div>Time: {{time}}</div>
</div>
The result would be a concatenation of the 2 divs:
<div htmx-swap-oob="true" id="one">
<div>Time: 1346876956</div>
</div>
<div htmx-swap-oob="true" id="two">
<div>Time: 1346876987</div>
</div>