Skip to content

Functional HTMX Endpoints #104

@tschuehly

Description

@tschuehly

I propose adding a HtmxEndpoint Class that can be used to create HTTP Endpoints and can be directly called from Template Engines.

The Controller would look like this:

@Controller 
class ExampleController {
  public HtmxEndpoint<UserForm> createUserEndpoint = new HtmxEndpoint<>(
      "/createUser",
      HttpMethod.POST,
      this::createUser
  );

  private ModelAndView createUser(UserForm userForm) {
    return new ModelAndView("createUser", Map.of("createUserEndpoint", createUserEndpoint));
  }
}

The class would like this:

public class HtmxEndpoint<T> implements RouterFunction<ServerResponse> {

  private final String path;
  private final HttpMethod method;
  private final Supplier<ModelAndView> modelAndViewSupplier;
  private final Function<T, ModelAndView> function;

  ParameterizedTypeReference<T> requestType = new ParameterizedTypeReference<>() {
  };

  public HtmxEndpoint(String path, HttpMethod method, Function<T, ModelAndView> function) {
    this.path = path;
    this.method = method;
    this.function = function;
    this.modelAndViewSupplier = null;
  }

  public HtmxEndpoint(String path, HttpMethod method, Supplier<ModelAndView> modelAndViewSupplier) {
    this.path = path;
    this.method = method;
    this.modelAndViewSupplier = modelAndViewSupplier;
    this.function = null;
    this.requestType = null;
  }

  @NotNull
  @Override
  public Optional<HandlerFunction<ServerResponse>> route(@NotNull ServerRequest request) {
    RequestPredicate predicate = RequestPredicates.method(method).and(RequestPredicates.path(path));
    if (predicate.test(request)) {
      ModelAndView modelAndView = getBody(request);
      return Optional.of(
          req -> RenderingResponse.create(modelAndView.view())
              .modelAttribute(modelAndView.model())
              .build()
      );
    }
    return Optional.empty();
  }

  private ModelAndView getBody(ServerRequest req) {
    if (function == null) {
      return modelAndViewSupplier.get();
    }

    try {
      return function.apply(
          req.body(requestType)
      );
    } catch (ServletException | IOException e) {
      throw new RuntimeException(e);
    }
  }

  public String call() {
    return "hx-" + method.name().toLowerCase() + " =\"" + path + "\"";
  }

}

In the template you would call it like this in Thymeleaf:

<div th:hx=${createUserEndpoint}>
<div>

And this template would render like this:

<div hx-post="/createUser">
<div>

Of course, the HtmxEndpoint could be expanded to all the possible Htmx attributes.

The Endpoints would be scanned at Startup using Reflection and added to the Spring RouterFunction

 @Bean
  ApplicationRunner applicationRunner() {
    return args -> {
      applicationContext.getBeansWithAnnotation(Controller.class)
          .values().forEach(controller ->
              {
                List<Field> fieldList = Arrays.stream(controller.getClass().getDeclaredFields())
                    .filter(method -> method.getType() == HtmxEndpoint.class)
                    .toList();

                fieldList.forEach(field -> {
                  RouterFunction<?> function = (RouterFunction<?>) ReflectionUtils.getField(field, controller);
                  if(routerFunctionMapping.getRouterFunction() == null){
                    routerFunctionMapping.setRouterFunction(function);
                  }
                  RouterFunction<?> routerFunction = routerFunctionMapping.getRouterFunction().andOther(function);
                  routerFunctionMapping.setRouterFunction(routerFunction);
                });
              }
          );
    };
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions