-
Notifications
You must be signed in to change notification settings - Fork 7
Integrate Thymeleaf and htmx #52
Description
Enhance the starter by using Thymeleaf template engine and htmx library:-
- To demonstrate the power of flyspring+flyfly, use thymeleaf templates that are also using htmx in our starter. Make sure to not create unnecessary configuration, service or repository classes. All the heavy lifting should be done by Flyspring.
- Also make sure to remove any leftover JARs before filing a PR.
Why stick to Thymeleaf and not use Groovy template engine?
Flyspring was developed using reactive framework (WebFlux) and Groovy templates are not directly supported out-of-the-box with Spring WebFlux. Although you can create a custom configuration using Groovy templates, configuring the resolver correctly to use the templates will create bean conflicts. In a very unlikely event of setting up the resolver, configurer, etc. for our template correctly, code complexity will skyrocket.
Here I am leaving my conversations with multiple GPT-4 instances for solving this problem:-
FIRST CONVERSATION
USER
I have this class :
package com.application.project.myapi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.util.*;
import java.util.stream.Collectors;
@RestController
public class UserController {
private List<User> users = new ArrayList<>();
@GetMapping("/")
public ModelAndView index() {
return new ModelAndView("index");
}
@GetMapping("/users")
public ModelAndView getUsers() {
List<User> usersList = User.getUsers();
List<Map<String, Object>> usersData = usersList.stream().map(User::toMap).collect(Collectors.toList());
// Column names
List<Map<String, String>> columnNames = Arrays.asList(
Map.of("title", "ID", "field", "id"),
Map.of("title", "Name", "field", "name"),
Map.of("title", "Email", "field", "email")
);
ModelAndView modelAndView = new ModelAndView("users");
modelAndView.addObject("data", usersData);
modelAndView.addObject("columns", columnNames);
return modelAndView;
}
@GetMapping("/htmx-example")
public String htmxExample() {
return "Hello! This is the HTMX example response.";
}
@PostMapping("/users")
public ModelAndView addUser(@RequestParam String name, @RequestParam String email) {
users.add(new User(name, email));
ModelAndView modelAndView = new ModelAndView("users");
modelAndView.addObject("users", users);
List<String> columnNames = Arrays.asList("Name", "Email");
modelAndView.addObject("columnNames", columnNames);
return modelAndView;
}
}
Here I am using modelandview but it is importing import org.springframework.web.servlet.ModelAndView that uses dependency implementation 'org.springframework.boot:spring-boot-starter-web'.
Change this class to only use implementation 'org.springframework.boot:spring-boot-starter-webflux' (gradle).
ASSISTANT
package com.application.project.myapi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.stream.Collectors;
@RestController
public class UserController {
private List<User> users = new ArrayList<>();
@GetMapping("/")
public Mono<String> index() {
return Mono.just("index");
}
@GetMapping("/users")
public Mono<Map<String, Object>> getUsers() {
List<User> usersList = User.getUsers();
List<Map<String, Object>> usersData = usersList.stream().map(User::toMap).collect(Collectors.toList());
// Column names
List<Map<String, String>> columnNames = Arrays.asList(
Map.of("title", "ID", "field", "id"),
Map.of("title", "Name", "field", "name"),
Map.of("title", "Email", "field", "email")
);
Map<String, Object> model = new HashMap<>();
model.put("data", usersData);
model.put("columns", columnNames);
return Mono.just(model);
}
@GetMapping("/htmx-example")
public Mono<String> htmxExample() {
return Mono.just("Hello! This is the HTMX example response.");
}
@PostMapping("/users")
public Mono<Map<String, Object>> addUser(@RequestParam String name, @RequestParam String email) {
users.add(new User(name, email));
List<Map<String, Object>> usersData = users.stream().map(User::toMap).collect(Collectors.toList());
// Column names
List<Map<String, String>> columnNames = Arrays.asList(
Map.of("title", "ID", "field", "id"),
Map.of("title", "Name", "field", "name"),
Map.of("title", "Email", "field", "email")
);
Map<String, Object> model = new HashMap<>();
model.put("data", usersData);
model.put("columns", columnNames);
return Mono.just(model);
}
}
USER
can this mono replace modelandview? Do i need to do any other changes in my project elsewhere? I am using groovy templates and that's why I was using modelandview
ASSISTANT
Yes, Mono can replace ModelAndView for returning data in a reactive way. Instead of using a view resolver to render the view, you can use a reactive template engine like Thymeleaf or Mustache to render the view.
If you are using Groovy templates, you can use the Groovy Template Engine from Spring Boot starter dependency org.springframework.boot:spring-boot-starter-groovy-templates.
Here's an example of using the Groovy Template Engine in a reactive way:
<!-- users.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users List</title>
</head>
<body>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<g:each in="${users}" var="user">
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
</g:each>
</tbody>
</table>
</body>
</html>
// UserController.java
@RestController
public class UserController {
private List<User> users = new ArrayList<>();
@GetMapping("/users")
public Mono<String> getUsers() {
List<User> usersList = User.getUsers();
Map<String, Object> model = new HashMap<>();
model.put("users", usersList);
return Mono.just("users").map(viewName -> new GroovyTemplateRenderer(viewName).render(model));
}
@PostMapping("/users")
public Mono<String> addUser(@RequestParam String name, @RequestParam String email) {
users.add(new User(name, email));
List<User> usersList = User.getUsers();
Map<String, Object> model = new HashMap<>();
model.put("users", usersList);
return Mono.just("users").map(viewName -> new GroovyTemplateRenderer(viewName).render(model));
}
}
Note that you need to update the return type of the methods to Mono<String> instead of ModelAndView.
USER
package com.application.project.myapi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.stream.Collectors;
@RestController
public class UserController {
private List<User> users = new ArrayList<>();
@GetMapping("/")
public Mono<String> index() {
return Mono.just("index");
}
@GetMapping("/users")
public Mono<Map<String, Object>> getUsers() {
List<User> usersList = User.getUsers();
List<Map<String, Object>> usersData = usersList.stream().map(User::toMap).collect(Collectors.toList());
// Column names
List<Map<String, String>> columnNames = Arrays.asList(
Map.of("title", "ID", "field", "id"),
Map.of("title", "Name", "field", "name"),
Map.of("title", "Email", "field", "email")
);
Map<String, Object> model = new HashMap<>();
model.put("data", usersData);
model.put("columns", columnNames);
return Mono.just(model);
}
@GetMapping("/htmx-example")
public Mono<String> htmxExample() {
return Mono.just("Hello! This is the HTMX example response.");
}
@PostMapping("/users")
public Mono<Map<String, Object>> addUser(@RequestParam String name, @RequestParam String email) {
users.add(new User(name, email));
List<Map<String, Object>> usersData = users.stream().map(User::toMap).collect(Collectors.toList());
// Column names
List<Map<String, String>> columnNames = Arrays.asList(
Map.of("title", "ID", "field", "id"),
Map.of("title", "Name", "field", "name"),
Map.of("title", "Email", "field", "email")
);
Map<String, Object> model = new HashMap<>();
model.put("data", usersData);
model.put("columns", columnNames);
return Mono.just(model);
}
}
These requests are not using groovy templates that are already present in resource/templates
ASSISTANT
In that case, you can use a reactive template engine like Thymeleaf or Mustache to render the view in a reactive way. Here's an example of using Thymeleaf with WebFlux:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.reactive.max-chunk-size=1024 * 1024
<!-- users.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users List</title>
</head>
<body>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}"></td>
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
</tr>
</tbody>
</table>
</body>
</html>
// UserController.java
@RestController
public class UserController {
private List<User> users = new ArrayList<>();
@GetMapping("/users")
public Mono<String> getUsers(Model model) {
List<User> usersList = User.getUsers();
model.addAttribute("users", usersList);
return Mono.just("users");
}
@PostMapping("/users")
public Mono<String> addUser(@RequestParam String name, @RequestParam String email, Model model) {
users.add(new User(name, email));
List<User> usersList = User.getUsers();
model.addAttribute("users", usersList);
return Mono.just("users");
}
}
Note that you need to update the return type of the methods to Mono<String> and add a Model parameter to the methods. The Model object is used to pass data to the view. Also, don't forget to configure the Thymeleaf properties in application.properties.
USER
don't use thymeleaf. Stick to groovy only. I already have my groovy templates, I just want my controller to use them using webflux.
ASSISTANT
I apologize for the confusion. Unfortunately, Spring Boot and WebFlux do not provide built-in support for Groovy-based reactive templates. Spring WebFlux currently supports Thymeleaf and Mustache for reactive template rendering.
However, you can still use Groovy templates in a non-reactive way with Spring MVC (spring-boot-starter-web) and ModelAndView. If you want to stick with Groovy templates, you'll need to use Spring MVC instead of WebFlux.
If you still want to use WebFlux with Groovy templates, you can try rendering the templates manually using the Groovy Template engine, but this may not provide the full benefits of a reactive application.
USER
can i use both webflux and web in a spring boot project?
ASSISTANT
Yes, you can use both Spring Web MVC (spring-boot-starter-web) and Spring WebFlux (spring-boot-starter-webflux) in a single Spring Boot project. However, it's essential to understand that mixing both technologies can lead to unexpected behavior and might not provide the full benefits of a reactive application.
To use both in a single project, you can include both dependencies in your build.gradle file:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}When using both Web MVC and WebFlux, you should separate the configuration for each framework to avoid conflicts. For example, you can have separate packages for Web MVC controllers and WebFlux handlers.
You can use @Controller or @RestController annotations for Web MVC controllers and @RequestMapping or other HTTP method-specific annotations (e.g., @GetMapping, @PostMapping) for defining endpoints.
For WebFlux, you can use functional routing with RouterFunction and HandlerFunction or use annotated controllers with @RestController and @RequestMapping. However, you should avoid mixing Web MVC and WebFlux annotations in the same class.
Please note that using both frameworks in a single project can be more complex to maintain and might lead to confusion in the long run. It's generally recommended to choose one framework (Web MVC or WebFlux) based on your project requirements and stick to it consistently across the application.