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
Copy @ 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
Copy @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.
Copy @ 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}
Copy @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
Copy @ 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:
Copy <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
Copy @ 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
Copy < main >
${groupManagementContext.viewContext()}
</ main >
Now we need to add the /group-management
endpoint to the GroupController
:
Copy @ 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
:
Copy 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:
Copy @ 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
Copy @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
Copy 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 HtmxUtil
and add a new <td>
in the @for
loop.
Copy @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-target
we 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