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.


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