I'm in the process of learning Spring Webflux.
I've created a little project which render a flux in a Thymeleaf template. This works well, but I would also like to display kind of a loader in the template while the data from the flux are processed.
For this I've initialized the flag value of the loader to true
outside of my flux and I switch the value to false
on the doOnComplete
method of this flux.
The problem is that my flag value never change in the template (but it does on the controller/router).
There is obviously something I'm missing. My guess is that the template have no way to know that my flag changed, but I was not able to understand how to do this.
How to trigger this change in the template ? Is it possible to mix a reactive value (flux) and a non-reactive value in the same Thymeleaf template ?
So far here is my code.
The router/handler
@Configuration
public class PersonRouter {
@Bean
public RouterFunction<ServerResponse> personRoutes(PersonHandler personHandler) {
return route(GET("/"), personHandler::renderPersonsAge);
}
}
@Component
@RequiredArgsConstructor
public class PersonHandler {
public static final String API_AGIFY_URI = "https://api.agify.io/";
private final PersonRepository personRepository;
public Mono<ServerResponse> renderPersonsAge(ServerRequest ignoredServerRequest) {
var modelMap = new ModelMap();
var isLoading = new AtomicBoolean(true);
var ages = this.fetchPersonAge()
.doOnComplete(() -> isLoading.set(false));
var contextVariable = new ReactiveDataDriverContextVariable(ages, 1);
modelMap.addAttribute("isLoading", isLoading.get());
modelMap.addAttribute("ages", contextVariable);
return ServerResponse.ok()
.contentType(MediaType.TEXT_HTML)
.render("index", modelMap);
}
private Flux<Age> fetchPersonsAge() {
return this.personRepository
.findAll()
.concatMap(this::guessPersonAge)
.delayElements(Duration.ofNanos(700));
}
private Mono<Age> guessPersonAge(Person person) {
return WebClient.create().get()
.uri(API_AGIFY_URI + "?name={name}", person.getName())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Age.class);
}
}
The template:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<title>Spring Reactive</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" th:href="@{/main.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/5.2.3/css/bootstrap.css}"/>
</head>
<body>
<main>
<div class="card">
<h5 class="card-header">Average age of a person according to his/her name</h5>
<div class="card-body card__center">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Count</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr data-th-each="age: ${ages}">
<td th:text="${age.name}"></td>
<td th:text="${age.count}"></td>
<td th:text="|${age.age} years|"></td>
</tr>
</tbody>
</table>
<div th:if="${isLoading}" class="spinner-border" role="status"></div>
</div>
</div>
</main>
</body>
</html>
Thanks for any kind of help