From d66ed8cb0e4c48f6cfb8964ba1fe986eb790bdaa Mon Sep 17 00:00:00 2001 From: Clint Checketts Date: Wed, 7 Aug 2024 07:55:29 -0600 Subject: [PATCH 1/4] Add support for processing HtmxResponse in the Model and as an Argument --- .../boot/mvc/HtmxHandlerInterceptor.java | 30 ++++++++++++++++++- .../boot/mvc/HtmxMvcAutoConfiguration.java | 9 ++++-- ...ResponseHandlerMethodArgumentResolver.java | 26 ++++++++++++++++ ...sponseHandlerMethodReturnValueHandler.java | 4 +-- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java index c393a03d..40aa833a 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java @@ -15,13 +15,41 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; public class HtmxHandlerInterceptor implements HandlerInterceptor { private final ObjectMapper objectMapper; + private final HtmxResponseHandlerMethodReturnValueHandler htmxResponseHandlerMethodReturnValueHandler; - public HtmxHandlerInterceptor(ObjectMapper objectMapper) { + public HtmxHandlerInterceptor(ObjectMapper objectMapper, HtmxResponseHandlerMethodReturnValueHandler htmxResponseHandlerMethodReturnValueHandler) { this.objectMapper = objectMapper; + this.htmxResponseHandlerMethodReturnValueHandler = htmxResponseHandlerMethodReturnValueHandler; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + if(modelAndView != null) { + modelAndView.getModel().values().forEach( + value ->{ + if(value instanceof HtmxResponse) { + buildAndRender((HtmxResponse) value, modelAndView, request, response); + } else if (value instanceof HtmxResponse.Builder) { + buildAndRender(((HtmxResponse.Builder) value).build(), modelAndView, request, response); + } + }); + } + } + + private void buildAndRender(HtmxResponse htmxResponse, ModelAndView mav, HttpServletRequest request, HttpServletResponse response) { + View v = htmxResponseHandlerMethodReturnValueHandler.toView(htmxResponse); + try { + v.render(mav.getModel(), request, response); + htmxResponseHandlerMethodReturnValueHandler.addHxHeaders(htmxResponse, response); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java index c59806e5..bac5dee4 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java @@ -43,16 +43,21 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper)); + registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper, createHtmxReponseHandler())); } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(new HtmxHandlerMethodArgumentResolver()); + resolvers.add(new HtmxResponseHandlerMethodArgumentResolver()); } @Override public void addReturnValueHandlers(List handlers) { - handlers.add(new HtmxResponseHandlerMethodReturnValueHandler(resolver.getObject(), locales, objectMapper)); + handlers.add(createHtmxReponseHandler()); + } + + private HtmxResponseHandlerMethodReturnValueHandler createHtmxReponseHandler() { + return new HtmxResponseHandlerMethodReturnValueHandler(resolver.getObject(), locales, objectMapper); } } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java new file mode 100644 index 00000000..8072e9c7 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java @@ -0,0 +1,26 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +public class HtmxResponseHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(HtmxResponse.Builder.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HtmxResponse.Builder htmxResponseBuilder = HtmxResponse.builder(); + if(mavContainer != null) { + mavContainer.addAttribute(htmxResponseBuilder); + } + return htmxResponseBuilder; + } +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java index 5755fc84..fbd4f47e 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java @@ -50,7 +50,7 @@ public void handleReturnValue(Object returnValue, addHxHeaders(htmxResponse, webRequest.getNativeResponse(HttpServletResponse.class)); } - private View toView(HtmxResponse htmxResponse) { + View toView(HtmxResponse htmxResponse) { Assert.notNull(htmxResponse, "HtmxResponse must not be null!"); @@ -74,7 +74,7 @@ private View toView(HtmxResponse htmxResponse) { }; } - private void addHxHeaders(HtmxResponse htmxResponse, HttpServletResponse response) { + void addHxHeaders(HtmxResponse htmxResponse, HttpServletResponse response) { addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal()); addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal()); addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal()); From ef427c8f56ff15c69f419e272672266d5876078e Mon Sep 17 00:00:00 2001 From: Clint Checketts Date: Wed, 7 Aug 2024 08:17:08 -0600 Subject: [PATCH 2/4] Add tests for argument provided HtmxResponse.Builder --- ...ResponseHandlerMethodReturnValueHandlerController.java | 8 ++++++++ .../HtmxResponseHandlerMethodReturnValueHandlerTest.java | 7 +++++++ .../src/test/resources/templates/argument.html | 0 3 files changed, 15 insertions(+) create mode 100644 htmx-spring-boot/src/test/resources/templates/argument.html diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java index c4292f7c..308a6759 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java @@ -137,6 +137,14 @@ public void throwException() { throw new RuntimeException("Fake exception"); } + @GetMapping("/argument") + public String argument(HtmxResponse.Builder htmxResponse) { + htmxResponse.trigger("event1"); + return "argument"; + } + + + @ExceptionHandler(Exception.class) public HtmxResponse handleError(Exception ex) { return HtmxResponse.builder() diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java index 099c554c..de6452a4 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java @@ -143,4 +143,11 @@ public void testException() throws Exception { Fake exception """); } + + @Test + public void testHxTriggerArgument() throws Exception { + mockMvc.perform(get("/hvhi/argument")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Trigger", "event1")); + } } diff --git a/htmx-spring-boot/src/test/resources/templates/argument.html b/htmx-spring-boot/src/test/resources/templates/argument.html new file mode 100644 index 00000000..e69de29b From 53cface24e00b1ea1a213b7dd960a93ce8ec650b Mon Sep 17 00:00:00 2001 From: Clint Checketts Date: Wed, 28 Aug 2024 06:38:28 -0600 Subject: [PATCH 3/4] Add readme example of using HtmxResponse.Builder as an argument --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 2a6a4673..3111bb00 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,35 @@ public HtmxResponse getMainAndPartial(Model model){ Using `ModelAndView` means that each fragment can have its own model (which is merged with the controller model before rendering). +### HtmxResponse.Builder as an argument + +An `HtmxReponse.Builder` can be injected as a controller method. This creates the parameter and adds it to the model, +allowing it to be used without requiring it be the method return value. This is useful when the return value is needed for +the template. + +This allows for the following usage: + +```java +@GetMapping("/endpoint") +public String endpoint(HtmxResponse.Builder htmxResponse, Model model) { + htmxResponse.trigger("event1"); + model.addAttribute("aField", "aValue"); + return "endpointTemplate"; +} +``` + +For example the JTE templating library supports statically typed templates and can be used like so: + +```java +@GetMapping("/endpoint") +public JteModel endpoint(HtmxResponse.Builder htmxResponse) { + htmxResponse.trigger("event1"); + String aField = "aValue"; + return templates.endpointTemplate(aField); +} +``` + + ### Error handlers It is possible to use `HtmxResponse` as a return type from error handlers. From 54fb71d7c885a21de46f492c4375909fcf47b360 Mon Sep 17 00:00:00 2001 From: Clint Checketts Date: Wed, 28 Aug 2024 06:39:40 -0600 Subject: [PATCH 4/4] Add link to JTE --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3111bb00..76a1e7d6 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ public String endpoint(HtmxResponse.Builder htmxResponse, Model model) { } ``` -For example the JTE templating library supports statically typed templates and can be used like so: +For example the [JTE templating library](https://jte.gg/) supports statically typed templates and can be used like so: ```java @GetMapping("/endpoint")