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",role="primary"]
|
||
|
----
|
||
|
@GetMapping
|
||
|
List<ModelAndView> handle() {
|
||
|
return List.of(new ModelAndView("posts"), new ModelAndView("comments"));
|
||
|
}
|
||
|
----
|
||
|
|
||
|
Kotlin::
|
||
|
+
|
||
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||
|
----
|
||
|
@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",role="primary"]
|
||
|
----
|
||
|
@GetMapping
|
||
|
FragmentsRendering handle() {
|
||
|
return FragmentsRendering.with("posts").fragment("comments").build();
|
||
|
}
|
||
|
----
|
||
|
|
||
|
Kotlin::
|
||
|
+
|
||
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||
|
----
|
||
|
@GetMapping
|
||
|
fun handle(): FragmentsRendering {
|
||
|
return FragmentsRendering.with("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",role="primary"]
|
||
|
----
|
||
|
@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",role="secondary"]
|
||
|
----
|
||
|
@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`.
|