Lab 3: Inline Editing

In this lab, we will create a group Management Page where we can add a user to a group and a navigation bar

Group Management

We start by creating a GroupManagementComponent ViewComponent in the de.tschuehly.easy.spring.auth.group.management package:

GroupManagementComponent.java
@ViewComponent
public class GroupManagementComponent {
  public static final String MODAL_CONTAINER_ID = "modalContainer";
  public static final String CLOSE_MODAL_EVENT = "close-modal";

  public record GroupManagementContext() implements ViewContext {  }

  public ViewContext render() {
    return new GroupManagementContext();
  }
}

The template is the same as the UserManagementComponent.jte but we added two <a> links to the <nav> element.

GroupManagementComponent.jte
@import static de.tschuehly.easy.spring.auth.group.management.GroupManagementComponent.CLOSE_MODAL_EVENT
@import static de.tschuehly.easy.spring.auth.group.management.GroupManagementComponent.MODAL_CONTAINER_ID
@import de.tschuehly.easy.spring.auth.group.management.GroupManagementComponent.GroupManagementContext
@param GroupManagementContext groupManagementContext
<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/livereload.js"></script>
</head>
<body hx-ext="debug">
<nav>
    <h1>
        Easy Spring Auth
    </h1>
    <a href="/">UserManagement</a>
    <a href="/group-management">GroupManagement</a>
    <hr>
</nav>
<main>

</main>
</body>
<div id="${MODAL_CONTAINER_ID}" hx-on:$unsafe{CLOSE_MODAL_EVENT}="this.innerHTML = null">

</div>
</html>

Next, we will create a GroupTableComponent in the de.tschuehly.easy.spring.auth.group.management.table package.

We autowire the groupService and create a GROUP_TABLE_ID constant.

GroupTableComponent.java
@ViewComponent
public class GroupTableComponent {

  private final GroupService groupService;

  public final static String GROUP_TABLE_ID = "groupTable";

  public record GroupTableContext() implements ViewContext { }

  public ViewContext render() {
    return new GroupTableContext();
  }
  
  public GroupTableComponent(GroupService groupService) {
    this.groupService = groupService;
  }
}

We add the corresponding template GroupTableComponent.jte and set the <table> id to ${GROUP_TABLE_ID}

GroupTableComponent.jte
@import static de.tschuehly.easy.spring.auth.group.management.table.GroupTableComponent.*
@param de.tschuehly.easy.spring.auth.group.management.table.GroupTableComponent.GroupTableContext groupTableContext
<table id="${GROUP_TABLE_ID}">
    <thead>
    <tr>
        <th>
            Group Name
        </th>
        <th>
            Group Members
        </th>
        <th>

        </th>
    </tr>
    </thead>
    <tbody>
    </tbody>
</table>

Now back in the GroupTableComponent.java, we retrieve all groups with the groupService.getAll() method and add this List of groups to the ViewContext

GroupTableComponent.java
@ViewComponent
public class GroupTableComponent {
  private final GroupService groupService;

  public record GroupTableContext(List<EasyGroup> groupList) 
    implements ViewContext{}

  public final static String GROUP_TABLE_ID = "groupTable";
  public ViewContext render(){
    List<EasyGroup> groupList = groupService.getAll();
    return new GroupTableContext(groupList);
  }

  public GroupTableComponent(GroupService groupService) {
    this.groupService = groupService;
  }
}

Now we need to replace the <tbody> element of the GroupTableComponent.jte with the following:

GroupTableComponent.jte
<tbody>
@for(var group: groupTableContext.groupList()) <%-- (1) --%>
    <tr>
        <td>
            ${group.groupName} <%-- (2) --%>
        </td>
        <td>
            @for(var member: group.memberList) <%-- (3) --%>
                <span>${member.username}</span> <%-- (4) --%>
            @else
                <span>no member</span> <%-- (5) --%>
            @endfor
        </td>
    </tr>
@endfor
</tbody>

(1): We loop over the groupTableContext.groupList() variable with the @for syntax

(2): We show the groupName in a <td>

(3): We loop over the group.memberList

(4): We show the username in a <span>

(5): If the group has no users we show a no member message


Then we render the GroupTableComponent in the GroupManagementComponent. We autowire it and pass it into the ViewContext

GroupManagementComponent.java
@ViewComponent
public class GroupManagementComponent {
  private final GroupTableComponent groupTableComponent;

  public static final String MODAL_CONTAINER_ID = "modalContainer";
  public static final String CLOSE_MODAL_EVENT = "close-modal";

  public GroupManagementComponent(GroupTableComponent groupTableComponent) {
    this.groupTableComponent = groupTableComponent;
  }

  public record GroupManagementContext(ViewContext viewContext) 
      implements ViewContext{}

  public ViewContext render(){
    return new GroupManagementContext(groupTableComponent.render());
  }
}

In the GroupManagementComponent.jte template we render the GroupTableComponent it in the <main> element, by using the viewContext

GroupManagementComponent.jte
<main>
    ${groupManagementContext.viewContext()}
</main>

Now we need to add the /group-management endpoint to the GroupController:

GroupController.java
@Controller
public class GroupController {
    private final GroupService groupService;
    private final GroupManagementComponent groupManagementComponent; // (1)

    public GroupController(GroupService groupService, GroupManagementComponent groupManagementComponent) {
        this.groupService = groupService; 
        this.groupManagementComponent = groupManagementComponent; // (1)
    }

    @GetMapping("/group-management") // (2)
    public ViewContext groupManagementComponent() {
        return groupManagementComponent.render(); // (3)
    }
}

(1): We autowire the GroupManagementComponent

(2): We create a new @GetMapping

(3): We return the ViewContext retrieved by calling the render() method

If we now run Lab3Application.java and navigate to localhost:8080/group-management to see the rendered groups and members.

Lab-3 Checkpoint 1

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

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

We haven't done anything new yet, now we are going to start with the inline editing feature.

Inline Editing

We now want to add a new User to one of the groups.

We autowire the GroupTableComponent and create an endpoint in the GroupController:

GroupController.java
public final static String POST_ADD_USER = "/group/{groupName}/add-user"; // (1)
public final static String USER_ID_PARAM = "userId"; // (2)
@PostMapping(POST_ADD_USER)
public ViewContext addUser(@PathVariable String groupName, // (3)
    @RequestParam(USER_ID_PARAM) UUID userId){ // (4)
  groupService.addUserToGroup(groupName,userId); // (5)
  return groupTableComponent.render(); // (6)
}  

(1): We define a POST_ADD_USER constant

(2): We define a USER_ID_PARAM constant

(3): We capture the groupName via @PathVariable

(4): We capture the userId via @RequestParam

(5): We add the user to the group via the groupService

(6): We return the GroupTableComponent.render which will render the group table with the new user added

Next, we create a AddUserComponent in the de.tschuehly.easy.spring.auth.group.management.table.user package:

AddUserComponent.java
@ViewComponent
public class AddUserComponent {
  private final UserService userService;

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

  public record AddUserContext(String groupName, List<EasyUser> easyUserList) 
      implements ViewContext{}

  public ViewContext render(String groupName){ // (2)
    return new AddUserContext(groupName,userService.findAll()); // (3)
  }
}

(1): We autowire the UserService

(2): The render method has a groupName parameter

(3): We pass the groupName and the userService.findAll to the AddUserContext.


Now we create a AddUserComponent.jte template in the same package as the AddUserComponent.java

AddUserComponent.jte
@import static de.tschuehly.easy.spring.auth.group.GroupController.*
@import de.tschuehly.easy.spring.auth.group.management.table.GroupTableComponent
@import de.tschuehly.easy.spring.auth.htmx.HtmxUtil
@param de.tschuehly.easy.spring.auth.group.management.table.user.AddUserComponent.AddUserContext addUserContext
<form hx-post="${HtmxUtil.URI(POST_ADD_USER,addUserContext.groupName())}" <%-- (1) --%>
      hx-target="${HtmxUtil.target(GroupTableComponent.GROUP_TABLE_ID)}" <%-- (2) --%>
      hx-swap="outerHTML"> <%-- (2) --%>
    <select name="${USER_ID_PARAM}">
        @for(var easyUser: addUserContext.easyUserList()) <%-- (3) --%>
            <option value="${easyUser.uuid.toString()}">
                ${easyUser.username}
            </option>
        @endfor
    </select>
    <button type="submit">Add User to group</button>
</form>

(1): We create a <form> element add an hx-post attribute that targets the POST_ADD_USER endpoint and inserts the groupName in the ViewContext into the Endpoint URI using the HtmxUtil

(2): We target the GROUP_TABLE_ID and swap the outerHTML of the target element.

(3): We create a <select> and use the @for loop syntax to create an option element for each user.

As you can see in contrast to the rerender method of the UserRowComponent here the htmx logic is in the template.


We now autowire the AddUserComponent and create a GET_SELECT_USER endpoint in the GroupController

GroupController.java
public final static String GET_SELECT_USER = "/group/{groupName}/select-user";

@GetMapping(GET_SELECT_USER)
public ViewContext selectUser(@PathVariable String groupName) {
  return addUserComponent.render(groupName);
}

Back to the GroupTableComponent.jte we add a static import to GET_SELECT_USER and HtmxUtiland add a new <td> in the @for loop.

GroupTableComponent.jte
@import static de.tschuehly.easy.spring.auth.group.management.table.GroupTableComponent.*
@import static de.tschuehly.easy.spring.auth.group.GroupController.GET_SELECT_USER
@import de.tschuehly.easy.spring.auth.htmx.HtmxUtil
@param de.tschuehly.easy.spring.auth.group.management.table.GroupTableComponent.GroupTableContext groupTableContext
<table id="${GROUP_TABLE_ID}">
    <thead>
    <tr>
        <th>
            Group Name
        </th>
        <th>
            Group Members
        </th>
        <th>

        </th>
    </tr>
    </thead>
    <tbody>
    @for(var group: groupTableContext.groupList())
        <tr>
            <td>
                ${group.groupName}
            </td>
            <td>
                @for(var member: group.memberList)
                    <span>${member.username}</span>
                @else
                    <span>no member</span>
                @endfor
            </td>
            <td>
                <button hx-get="${HtmxUtil.URI(GET_SELECT_USER,group.groupName)}" <%-- (1) --%>
                        hx-swap="outerHTML"> <%-- (2) --%>
                    <img src="/plus.svg">
                </button>
            </td>
        </tr>
    @endfor
    </tbody>
</table>

(1): We create a <button> element that has a hx-get attribute that creates a GET request to /group/groupName/select-user

(2): We swap the outerHTML of the target element. As we didn't set the hx-targetwe replace the <button> element.

Now restart the application and navigate to localhost:8080/group-management.

We can click on the plus and see the selector to add a User to the group. When clicking on Add User to group the table is rerendered with the updated value.

Lab-3 Checkpoint 2

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

git checkout tags/lab-3-checkpoint-1 -b lab-3-c2

Last updated