server-side-spring-htmx-workshop
  • Building server-side web applications with htmx
  • Lab 1: Server-side rendering with Spring Boot and JTE
  • Lab 2: Using Spring ViewComponent
  • Lab 3: Inline Editing
  • Lab 4: Using Spring Beans to Compose the UI
  • Lab 5: Lazy Loading
  • Lab 6: Full Text Search
  • Lab 7: Infinite Scroll
  • Lab 8: Exception Messages
  • Lab 9: Server-Sent Events
Powered by GitBook
On this page
  • An Introduction to Spring ViewComponent
  • Creating the UserMangement with Spring ViewComponent
  • UserTableComponent
  • Edit User
  • Fix the Save User functionality
  • HtmxUtil
  • Create User

Lab 2: Using Spring ViewComponent

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.

SimpleViewComponent.jte
@param SimpleViewComponent.SimpleViewContext simpleViewContext
<div>${simpleViewContext.helloWorld()}</div>

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.

build.gradle.kts
implementation("de.tschuehly:spring-view-component-jte:0.8.1")
implementation("io.github.wimdeblauwe:htmx-spring-boot:3.3.0")

We can enable live-reload for Spring ViewComponent with these properties in application.yaml.

Also, remove the gg.jte properties and uncomment the spring.view-component properties

application.yaml
spring:
  view-component:
    local-development: true
    viewComponentRoot: lab-2/src/main/java
#gg:
 # jte:
  #  developmentMode: true
   # templateLocation: lab-2/src/main/jte

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:

UserManagementComponent.jte
@import static de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.*
@import de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.UserManagementContext
@param UserManagementContext userManagementContext
<html lang="en">

<head>
    <title>Easy Spring Auth</title>
    <link rel="stylesheet" href="/css/sakura.css" type="text/css">
    <script src="/htmx_1.9.11.js"></script>
    <script src="/htmx_debug.js"></script>
    <script src="http://localhost:35729"></script>
</head>
<body hx-ext="debug">
<nav>
    <h1>
        Easy Spring Auth
    </h1>
</nav>
<main>
 
</main>
</body>
<div id="${MODAL_CONTAINER_ID}" hx-on:$unsafe{CLOSE_MODAL_EVENT}="this.innerHTML = null">
</div>
</html>

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:

UserTableComponent.jte
@import de.tschuehly.easy.spring.auth.user.management.table.UserTableComponent.UserTableContext
@import static de.tschuehly.easy.spring.auth.user.UserController.GET_CREATE_USER_MODAL
@import static de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.MODAL_CONTAINER_ID
@import static de.tschuehly.easy.spring.auth.user.management.table.UserTableComponent.USER_TABLE_BODY_ID
@param UserTableContext userTableContext
<table>
    <thead>
    <tr>
        <th>
            uuid
        </th>
        <th>
            username
        </th>
        <th>
            password
        </th>
    </tr>
    </thead>
    <tbody id="${USER_TABLE_BODY_ID}">
    
    </tbody>
    <tfoot>
    <tr>
        <td colspan="4">
            <button hx-get="${GET_CREATE_USER_MODAL}" 
                    hx-target="#${MODAL_CONTAINER_ID}">
                Create new User
            </button>
        </td>
    </tr>
    </tfoot>
</table>

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.

@import static de.tschuehly.easy.spring.auth.htmx.HtmxUtil.URI
@import static de.tschuehly.easy.spring.auth.user.UserController.*
@import static de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.MODAL_CONTAINER_ID
@import de.tschuehly.easy.spring.auth.user.management.table.row.UserRowComponent.UserRowContext
@param UserRowContext userRowContext

!{var uuid = userRowContext.easyUser().uuid;} <%-- (1) --%>
<tr id="${UserRowContext.htmlUserId(uuid)}"> <%-- (2) --%>
    <td>
        ${uuid.toString()}
    </td>
    <td>
        ${userRowContext.easyUser().username} <%-- (1) --%>
    </td>
    <td>
        ${userRowContext.easyUser().password} <%-- (1) --%>
    </td>
    <td>
        <button hx-get="${URI(GET_EDIT_USER_MODAL,uuid)}"
                hx-target="#${MODAL_CONTAINER_ID}">
            <img src="/edit.svg">
        </button>
    </td>
</tr>

(1): We use the userRowContext.easyUser() attribute to access the user we want to render.

(2): We set the id of the table row using the UserRowContext.htmlUserId(uuid) function

UserTableComponent

We now change the UserTableComponent.java:

UserTableComponent.java
@ViewComponent
public class UserTableComponent {
  private final UserService userService;
  private final UserRowComponent userRowComponent; // (1)

  public UserTableComponent(UserService userService, UserRowComponent userRowComponent) {
    this.userService = userService;
    this.userRowComponent = userRowComponent; // (1)
  }

  public record UserTableContext(List<ViewContext> userTableRowList) // (2)
    implements ViewContext{

  }
  public static final String USER_TABLE_BODY_ID = "userTableBody";

  public ViewContext render(){
    List<ViewContext> rowList = userService.findAll() // (3)
        .stream().map(userRowComponent::render).toList(); // (4)
    return new UserTableContext(rowList);
  }
}

(1): We autowire the userRowComponent.

(2): We add a List<ViewContext> property to the UserTableContext

(3): We call the userService.findAll() method

(4): Then we call the autowired userRowComponent::render method in the .stream().map() function.

Now we will render the userTableRowList in the UserTableComponent.jte:

UserTableComponent.jte
@import de.tschuehly.easy.spring.auth.user.management.table.UserTableComponent.UserTableContext
@import static de.tschuehly.easy.spring.auth.user.UserController.GET_CREATE_USER_MODAL
@import static de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.MODAL_CONTAINER_ID
@import static de.tschuehly.easy.spring.auth.user.management.table.UserTableComponent.USER_TABLE_BODY_ID
@param UserTableContext userTableContext
<table>
    <thead>
    <tr>
        <th>
            uuid
        </th>
        <th>
            username
        </th>
        <th>
            password
        </th>
    </tr>
    </thead>
    <tbody id="${USER_TABLE_BODY_ID}">
    @for(var row: userTableContext.userTableRowList()) <%-- (1) --%>
        ${row} <%-- (2) --%>
    @endfor
    </tbody>
    <tfoot>
    <tr>
        <td colspan="4">
            <button hx-get="${GET_CREATE_USER_MODAL}" hx-target="#${MODAL_CONTAINER_ID}">
                Create new User
            </button>
        </td>
    </tr>
    </tfoot>
</table>

(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:

UserManagementComponent.jte
@import static de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.*
@import de.tschuehly.easy.spring.auth.user.management.UserManagementComponent.UserManagementContext
@param UserManagementContext userManagementContext
<html lang="en">

<head>
    <title>Easy Spring Auth</title>
    <link rel="stylesheet" href="/css/sakura.css" type="text/css">
    <script src="/htmx_1.9.11.js"></script>
    <script src="/htmx_debug.js"></script>
    <script src="http://localhost:35729"></script>
</head>
<body hx-ext="debug">
<nav>
    <h1>
        Easy Spring Auth
    </h1>
</nav>
<main>
    ${userManagementContext.userTable()}
</main>
</body>
<div id="${MODAL_CONTAINER_ID}" hx-on:$unsafe{CLOSE_MODAL_EVENT}="this.innerHTML = null">

</div>

</html>

In the UserController.java we can autowire the UserManagementComponent ViewComponent and render it in the index method:

UserController.java
@Controller
public class UserController {

  private final UserService userService;
  private final UserManagementComponent userManagementComponent;

  public UserController(UserService userService, UserManagementComponent userManagementComponent) {
    this.userService = userService;
    this.userManagementComponent = userManagementComponent;
  }

  @GetMapping("/")
  public ViewContext index() {
    return userManagementComponent.render();
  }
}

Lab 2 Checkpoint 1

If you are stuck you can resume at this checkpoint with:

git checkout tags/lab-2-checkpoint-1 -b lab-2-c1

Edit User

We now create the edit user functionality with Spring ViewComponent.

We create an EditUserComponent.java in the de.tschuehly.easy.spring.auth.user.management.edit package:

EditUserComponent.java
@ViewComponent
public class EditUserComponent {

  private final UserService userService;

  public EditUserComponent(UserService userService) { // (1)
    this.userService = userService;
  }

  public ViewContext render(UUID uuid) { // (2)
    EasyUser user = userService.findById(uuid); // (3)
    return new EditUserContext(user.uuid, user.username, user.password); // (4)
  }
  
  public record EditUserContext(UUID uuid, String username, String password) 
    implements ViewContext {

  }
}

(1): We first autowire the userService in the constructor

(2): Then we create a render method with a uuid parameter.

(3): We get the user with the userService.findById(uuid) method

(4): We add the uuid, username and password of the user to the EditUserContext ViewContext


We then create the EditUserComponent.jte template in the same package as the EditUserComponent.java

EditUserComponent.jte
@import de.tschuehly.easy.spring.auth.user.management.edit.EditUserComponent.EditUserContext
@import static de.tschuehly.easy.spring.auth.user.UserController.POST_SAVE_USER
@param EditUserContext editUserContext

<div style="width: 100dvw; height: 100dvh; position: fixed; top: 0;left: 0; background-color: rgba(128,128,128,0.69); display: flex; justify-content: center; align-items: center;">
    <form style="background-color: whitesmoke; padding: 2rem;">
        <label>
            UUID
            <input type="text" readonly name="uuid" value="${editUserContext.uuid().toString()}">
        </label>
        <label>
            Username
            <input type="text" name="username" value="${editUserContext.username()}">
        </label>
        <label>
            Password
            <input type="text" name="password" value="${editUserContext.password()}">
        </label>
        <button type="submit" hx-post="${POST_SAVE_USER}">
            Save User
        </button>
    </form>
</div>

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);
}

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:

UserController.java
@PostMapping(POST_SAVE_USER)
public String saveUser(UUID uuid, String username, String password, Model model, HttpServletResponse response) {
  EasyUser user = userService.saveUser(uuid, username, password);
  model.addAttribute("easyUser", user);
  response.addHeader("HX-Retarget", "#user-" + user.uuid);
  response.addHeader("HX-Reswap", "outerHTML");
  response.addHeader("HX-Trigger", CLOSE_MODAL_EVENT);
  return "UserRow";
}

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

UserRowComponent.java
public ViewContext rerender(EasyUser easyUser) {
  String target = HtmxUtil.target(UserRowContext.htmlUserId(easyUser.uuid)); // (1)
  HtmxUtil.retarget(target); 
  HtmxUtil.reswap(HxSwapType.OUTER_HTML); // (2)
  HtmxUtil.trigger(UserManagementComponent.CLOSE_MODAL_EVENT); // (3)
  return new UserRowContext(easyUser); // (4)
}

(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);
}

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

CreateUserComponent.jte
@import static de.tschuehly.easy.spring.auth.user.UserController.POST_CREATE_USER
<div style="width: 100dvw; height: 100dvh; position: fixed; top: 0;left: 0; background-color: rgba(128,128,128,0.69); display: flex; justify-content: center; align-items: center;">
    <form style="background-color: whitesmoke; padding: 2rem;">
        <label>
            Username
            <input type="text" name="username">
        </label>
        <label>
            Password
            <input type="text" name="password">
        </label>
        <button type="submit" hx-post="${POST_CREATE_USER}">
            Save User
        </button>
    </form>
</div>

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();
}

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:

git checkout tags/lab-2-checkpoint-4 -b lab-2-c4

PreviousLab 1: Server-side rendering with Spring Boot and JTENextLab 3: Inline Editing

Last updated 7 months ago

We can restart the application, navigate to and see the table rendered.

We can restart the application navigate to and the edit user modal works again.

We are using Wim Deblauwes htmx-spring-boot library: . It offers a HtmxResponseHeader enum with all possible values and a HxSwapType enum.

We can restart the application and navigate to and the save user function works again!

We can restart the application and navigate to and the create user modal is shown when we click on Create User

http://localhost:8080/
http://localhost:8080/
github.com/wimdeblauwe/htmx-spring-boot
http://localhost:8080/
http://localhost:8080/