This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Framework 6.2.0!

HTML Fragments

HTMX and 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:

  • Java

  • Kotlin

@GetMapping
List<ModelAndView> handle() {
	return List.of(new ModelAndView("posts"), new ModelAndView("comments"));
}
@GetMapping
fun handle(): List<ModelAndView> {
	return listOf(ModelAndView("posts"), ModelAndView("comments"))
}

The same can be done also by returning the dedicated type FragmentsRendering:

  • Java

  • Kotlin

@GetMapping
FragmentsRendering handle() {
	return FragmentsRendering.with("posts").fragment("comments").build();
}
@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:

  • Java

  • Kotlin

@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;
}
@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.