This lab aims to build the same application as Lab 1.
But this time we will use Spring ViewComponent and htmx-spring-boot to delegate rendering responsibility to the ViewComponent.
An Introduction to Spring ViewComponent
A ViewComponent is a Spring-managed bean that defines a rendering context for a corresponding template, this context is called ViewContext.
We can create a ViewComponent by annotating a class with the @ViewComponent annotation and defining a public nested record that implements the ViewContext interface.
SimpleViewComponent.java
@ViewComponent
public class SimpleViewComponent {
public record SimpleViewContext(String helloWorld) implements ViewContext {
}
public SimpleView render() {
return new SimpleView("Hello World");
}
}
A ViewComponent needs to have a template with the same name defined in the same package. In the template, we can access the properties of the record.
Spring ViewComponent wraps the underlying MVC model using Spring AOP and enables us to create the frontend in a similar way to the component-oriented JavaScript frameworks
Creating the UserMangement with Spring ViewComponent
To start we need to add three dependencies to the build.gradle.kts file.
We start by creating a UserManagementComponent.java file in the de.tschuehly.easy.spring.auth.user.management package.
UserManagementComponent.java
@ViewComponent
public class UserManagementComponent {
public static final String MODAL_CONTAINER_ID = "modalContainer";
public static final String CLOSE_MODAL_EVENT = "close-modal";
public record UserManagementContext()
implements ViewContext{}
public ViewContext render(){
return new UserManagementContext();
}
}
We then create a UserManagementComponent.jte template in the de.tschuehly.easy.spring.auth.user.management package:
We add a static import to the UserManagementComponent class.
The param is a UserManagementContext as we put all variables into this record.
Now we will create a separate component for the table.
UserTableComponent
Create a UserTableComponent.java in de.tschuehly.easy.spring.auth.user.management.table
UserTableComponent.java
@ViewComponent
public class UserTableComponent {
private final UserService userService;
public UserTableComponent(UserService userService) {
this.userService = userService;
}
public record UserTableContext() implements ViewContext{
}
public static final String USER_TABLE_BODY_ID = "userTableBody";
public ViewContext render(){
return new UserTableContext();
}
}
In the last lab, we defined the USER_TABLE_BODY_ID in the UserController.java. Now define it in the UserTableComponent.java .
Now we will create a UserManagementComponent.jte template in the same package as the UserTableComponent.java:
We then create a UserRowComponent.java class in de.tschuehly.easy.spring.auth.user.management.table.row package:
UserRowComponent.java
@ViewComponent
public class UserRowComponent {
public record UserRowContext(EasyUser easyUser) implements ViewContext {
public static String htmlUserId(UUID uuid) {
return "user-" + uuid;
}
}
public ViewContext render(EasyUser easyUser) {
return new UserRowContext(easyUser);
}
}
We then create a UserRowComponent.jte template in the auth.user.management.table.row package.
(1): We loop through the userTableRowList and create a row loop variable.
(2): We render the row ViewContext in the <tbody>
Now we can render the table using Spring ViewComponent in the UserMangement.java:
UserMangement.java
@ViewComponent
public class UserManagementComponent {
public static final String MODAL_CONTAINER_ID = "modalContainer";
public static final String CLOSE_MODAL_EVENT = "close-modal";
private final UserTableComponent userTableComponent; // (1)
public UserManagementComponent(UserTableComponent userTableComponent) {// (1)
this.userTableComponent = userTableComponent;
}
public record UserManagementContext(ViewContext userTable)
implements ViewContext {}
public ViewContext render(){
return new UserManagementContext(userTableComponent.render());
}
}
(1): We autowire the UserTableComponent
(2): We add a userTable ViewContext field to the UserManagementContext
(3): In the UserManagementComponent.render method we call the userTableComponent.render and pass it into the UserManagementContext constructor.
In the UserManagementComponent.jte template, we can insert the rendered table:
In the UserController.java we remove the UserForm record, autowire the EditUserComponent and then change the editUserModal method to call the editUserComponent.render method.
UserController.java
public static final String GET_EDIT_USER_MODAL = "/save-user/modal/{uuid}";
@GetMapping(GET_EDIT_USER_MODAL)
public ViewContext editUserModal(@PathVariable UUID uuid) {
return editUserComponent.render(uuid);
}
We can restart the application navigate to http://localhost:8080/ and the edit user modal works again.
Lab 2 Checkpoint 2
If you are stuck you can resume at this checkpoint with:
git checkout tags/lab-2-checkpoint-2 -b lab-2-c2
Fix the Save User functionality
In Lab1 we used HX Response headers to set the swapping functionality directly in the UserController.java:
We now want to move this functionality to the UserRowComponent.
HtmxUtil
I have already created a HtmxUtil class in the de.tschuehly.easy.spring.auth.htmx package that helps us set the HX Response Headers.
We are using Wim Deblauwes htmx-spring-boot library: github.com/wimdeblauwe/htmx-spring-boot. It offers a HtmxResponseHeader enum with all possible values and a HxSwapType enum.
We will add these convenience methods to the HtmxUtil.java class:
HtmxUtil.java
public static String target(String id){
return "#" + id;
}
public static void retarget(String cssSelector) {
setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), cssSelector);
}
public static void reswap(HxSwapType hxSwapType){
setHeader(HtmxResponseHeader.HX_RESWAP.getValue(), hxSwapType.getValue());
}
public static void trigger(String event) {
setHeader(HtmxResponseHeader.HX_TRIGGER.getValue(), event);
}
Back to the UserRowComponent we create a rerender function where we use these utility functions:
(1): We retarget to the id of the <tr> element we created with the UserRowContext.htmlUserId() function.
(2): We swap the outerHTML of the target element
(3). And we trigger the CLOSE_MODAL_EVENT
(4): Finally, we return the UserRowContext with the easyUser
In the UserController.saveUser method we can call the userRowComponent.rerender method
UserController.java
@PostMapping(POST_SAVE_USER)
public ViewContext saveUser(UUID uuid, String username, String password) {
EasyUser user = userService.saveUser(uuid, username, password);
return userRowComponent.rerender(user);
}
We can restart the application and navigate to http://localhost:8080/ and the save user function works again!
Lab 2 Checkpoint 3
If you are stuck you can resume at this checkpoint with:
git checkout tags/lab-2-checkpoint-3 -b lab-2-c3
We have the advantage that the Controller doesn't need to know how the UserRowComponent template looks and what needs to be swapped.
The UserRowComponent offers an API to rerender a row.
Create User
Finally, we need to migrate the create user functionality to Spring ViewComponent.
We start by creating a CreateUserComponent in the de.tschuehly.easy.spring.auth.user.management.create package:
CreateUserComponent.java
@ViewComponent
public class CreateUserComponent {
public record CreateUserContext() implements ViewContext{}
public ViewContext render(){
return new CreateUserContext();
}
}
We now need to create a CreateUserComponent.jte in the same package as the CreateUserComponent.java
We can now call the createUserComponent.render method in the UserController.getCreateUserModal method:
UserController.java
public static final String GET_CREATE_USER_MODAL = "/create-user/modal";
@GetMapping(GET_CREATE_USER_MODAL)
public ViewContext getCreateUserModal() {
return createUserComponent.render();
}
We can restart the application and navigate to http://localhost:8080/ and the create user modal is shown when we click on Create User
Finally, we need to migrate the UserController.createUser method.
Currently, it looks like this:
UserController.java
@PostMapping(POST_CREATE_USER)
public String createUser(String username, String password, Model model, HttpServletResponse response) {
EasyUser user = userService.createUser(username, password);
model.addAttribute("easyUser", user);
response.addHeader("HX-Retarget", "#" + USER_TABLE_BODY_ID);
response.addHeader("HX-Reswap", "afterbegin");
response.addHeader("HX-Trigger", CLOSE_MODAL_EVENT);
return "UserRow";
}
As before we want to move this code into the UserRowComponent.java, by creating a new renderNewRow function:
UserRowComponent.java
public ViewContext renderNewRow(EasyUser user) {
String target = HtmxUtil.target(UserTableComponent.USER_TABLE_BODY_ID);
HtmxUtil.retarget(target);
HtmxUtil.reswap(HxSwapType.AFTER_BEGIN);
HtmxUtil.trigger(UserManagementComponent.CLOSE_MODAL_EVENT);
return new UserRowContext(user);
}
We can now simplify the UserController.createUser function:
UserController.java
@PostMapping(POST_CREATE_USER)
public ViewContext createUser(String username, String password) {
EasyUser user = userService.createUser(username, password);
return userRowComponent.renderNewRow(user);
}
Now if we restart the application we can save a new user and they are inserted at the start of the table.
Lab 2 Checkpoint 4
If you are stuck you can resume at this checkpoint with: