You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
118 lines
3.0 KiB
118 lines
3.0 KiB
[[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`.
|
|
|