118 lines
3.0 KiB
Plaintext
118 lines
3.0 KiB
Plaintext
[[mvc-view-fragments]]
|
|
= HTML Fragments
|
|
:page-section-summary-toc: 1
|
|
|
|
[.small]#xref:web/webflux-view.adoc#webflux-view-fragments[See equivalent in the Reactive stack]#
|
|
|
|
https://htmx.org/[HTMX] and https://turbo.hotwired.dev/[Hotwire Turbo] emphasize an
|
|
HTML-over-the-wire approach where clients receive server updates in HTML rather than in JSON.
|
|
This allows the benefits of an SPA (single page app) without having to write much or even
|
|
any JavaScript. For a good overview and to learn more, please visit their respective
|
|
websites.
|
|
|
|
In Spring MVC, view rendering typically involves specifying one view and one model.
|
|
However, in HTML-over-the-wire a common capability is to send multiple HTML fragments that
|
|
the browser can use to update different parts of the page. For this, controller methods
|
|
can return `Collection<ModelAndView>`. For example:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
List<ModelAndView> handle() {
|
|
return List.of(new ModelAndView("posts"), new ModelAndView("comments"));
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
fun handle(): List<ModelAndView> {
|
|
return listOf(ModelAndView("posts"), ModelAndView("comments"))
|
|
}
|
|
----
|
|
======
|
|
|
|
The same can be done also by returning the dedicated type `FragmentsRendering`:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
FragmentsRendering handle() {
|
|
return FragmentsRendering.fragment("posts").fragment("comments").build();
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
fun handle(): FragmentsRendering {
|
|
return FragmentsRendering.fragment("posts").fragment("comments").build()
|
|
}
|
|
----
|
|
======
|
|
|
|
Each fragment can have an independent model, and that model inherits attributes from the
|
|
shared model for the request.
|
|
|
|
HTMX and Hotwire Turbo support streaming updates over SSE (server-sent events).
|
|
A controller can use `SseEmitter` to send `ModelAndView` to render a fragment per event:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
SseEmitter handle() {
|
|
SseEmitter emitter = new SseEmitter();
|
|
startWorkerThread(() -> {
|
|
try {
|
|
emitter.send(SseEmitter.event().data(new ModelAndView("posts")));
|
|
emitter.send(SseEmitter.event().data(new ModelAndView("comments")));
|
|
// ...
|
|
}
|
|
catch (IOException ex) {
|
|
// Cancel sending
|
|
}
|
|
});
|
|
return emitter;
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
fun handle(): SseEmitter {
|
|
val emitter = SseEmitter()
|
|
startWorkerThread{
|
|
try {
|
|
emitter.send(SseEmitter.event().data(ModelAndView("posts")))
|
|
emitter.send(SseEmitter.event().data(ModelAndView("comments")))
|
|
// ...
|
|
}
|
|
catch (ex: IOException) {
|
|
// Cancel sending
|
|
}
|
|
}
|
|
return emitter
|
|
}
|
|
----
|
|
======
|
|
|
|
The same can also be done by returning `Flux<ModelAndView>`, or any other type adaptable
|
|
to a Reactive Streams `Publisher` through the `ReactiveAdapterRegistry`. |