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
  • Setup
  • InfiniteLoadComponent
  • UserController
  • UserTableComponent
  • UserManagementComponent
  • Sucess!

Lab 7: Infinite Scroll

PreviousLab 6: Full Text SearchNextLab 8: Exception Messages

Last updated 1 year ago

In Lab 5 we improved the perceived responsiveness of our application by lazy loading the user table.

Even without a simulated delay, the response was still quite slow as we transferred a 4.1 MB HTML table:

In this lab we will fix this by using pagination with the infinite scroll mechanism, only loading new users when the user wants to see them.

Setup

First, we add a paginated retrieval method to the UserService:

UserService.java
public List<EasyUser> getPage(int pageNumber, int pageSize) {
  var startIndex = pageNumber * pageSize;
  var endIndex = startIndex + pageSize;
  return easyUserList.subList(startIndex, endIndex);
}

We also add two new render methods to the ListComponent to allow us to combine ViewContextLists.

ListComponent.java
public ViewContext render(List<ViewContext> viewContextList, ViewContext... viewContext){
  ArrayList<ViewContext> combinedList  = new ArrayList<>();
  combinedList.addAll(viewContextList);
  combinedList.addAll(List.of(viewContext));
  return new ListContext(combinedList);
}

public ViewContext render( ViewContext... viewContext){
  return new ListContext(Arrays.stream(viewContext).toList());
}

InfiniteLoadComponent

We then create a InfiniteLoadCompoent.java ViewComponent in the de.tschuehly.easy.spring.auth.user.management.table.infinite package.

In the render method, we define a nextPage parameter that we can access in the ViewContext:

InfiniteLoadComponent.java
package de.tschuehly.easy.spring.auth.user.management.table.infinite;

import de.tschuehly.spring.viewcomponent.core.component.ViewComponent;
import de.tschuehly.spring.viewcomponent.jte.ViewContext;

@ViewComponent
public class InfiniteLoadComponent {

  public ViewContext render(int nextPage){
    return new InfiniteLoadContext(nextPage);
  }

  public record InfiniteLoadContext(int nextPage) 
    implements ViewContext {}
}

Then we will create a InfiniteLoadComponent.jte template in the same package:

InfiniteLoadComponent.jte
@import static de.tschuehly.easy.spring.auth.user.UserController.*
@import de.tschuehly.easy.spring.auth.htmx.HtmxUtil
@import de.tschuehly.easy.spring.auth.user.management.table.infinite.InfiniteLoadComponent.InfiniteLoadContext
@param InfiniteLoadContext infiniteLoadContext

<tr hx-get="${HtmxUtil.URI(GET_USER_TABLE,infiniteLoadContext.nextPage())}" <%-- (1) --%>
    hx-trigger="intersect once" <%-- (2) --%>
    hx-swap="outerHTML"> <%-- (3) --%>
</tr>

(1): We define an hx-getattribute that requests the GET_USER_TABLE endpoint with the nextPage viewContext property as URI variable.

(2): With hx-trigger="intersect once" we can tell htmx to create the request only once when the element intersects with the browser viewport.

(3): With hx-swap="outerHTML" we tell htmx to swap the whole <tr> element.

UserController

We now need to change the UserController GET_USER_TABLE endpoint to accept a page @PathVariable by both adjusting the constant and method

UserController.java
public static final String GET_USER_TABLE = "/user-table/{page}";

@HxRequest
@GetMapping(GET_USER_TABLE)
public ViewContext userTable(@PathVariable String page) {
  return userTableComponent.render(Integer.parseInt(page));
}

UserTableComponent

Next, we need to adjust the UserTableComponent to accept a page parameter:

UserTableComponent.java
public ViewContext render(int currentPage) {
  List<ViewContext> userRowList = userService.getPage(currentPage, 20) // (1)
      .stream().map(userRowComponent::render).toList(); // (2)
  ViewContext tableBody = listComponent.render(
      userRowList, // (3)
      infiniteLoadComponent.render(currentPage + 1) // (4)
  );
  if(currentPage == 0){
    return new UserTableContext(tableBody); // (5)
  }
  return tableBody; // (6)
}

public record UserTableContext(ViewContext userTableBody) // (7)
    implements ViewContext {}

(1): In the method, we first retrieve the page of users with the userService.getPage method.

(2): We then stream through the list of users and call the userRowComponent::render to get a ViewContext list

(3): Next we will call the listComponent.render method and pass the userRowList

(4): We also pass theinfiniteLoadComponent.render method with the currentPage increased by one.

(5): When the user is on the first page we want to render the full UserTableComponent

(6): But for each following page, we want to render the userRowList and the infiniteLoadComponent without the whole table structure by just passing the tableBody variable-

(7): We now need to change the UserTableContext to have a userTableBody parameter

Now in the UserTableComponent, we need to render the userTableBody ViewContext property in the <tbody> element.

UserTableComponent.jte
<tbody id="${USER_TABLE_BODY_ID}">
${userTableContext.userTableBody()}
</tbody>

UserManagementComponent

We need to adjust the UserMangement.jte template to pass in 0 as the parameter.

UserManagementComponent.jte
@import static de.tschuehly.easy.spring.auth.user.UserController.*
@import de.tschuehly.easy.spring.auth.htmx.HtmxUtil

<div hx-get="${HtmxUtil.URI(GET_USER_TABLE,0)}" 
     hx-trigger="load">
    <img alt="Result loading..." class="htmx-indicator" width="50" src="/spinner.svg"/>
</div>

Sucess!

Lab-7 Checkpoint 1

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

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

If we restart the application and navigate to we can see that the infinite scroll is working again!

http://localhost:8080