Skip to content

Commit b117795

Browse files
authored
Merge pull request #128 from wimdeblauwe/checketts/htmx-response
Add support for processing HtmxResponse in the Model and as an Argument
2 parents 69fb0e1 + 54fb71d commit b117795

File tree

8 files changed

+108
-5
lines changed

8 files changed

+108
-5
lines changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,35 @@ public HtmxResponse getMainAndPartial(Model model){
181181

182182
Using `ModelAndView` means that each fragment can have its own model (which is merged with the controller model before rendering).
183183

184+
### HtmxResponse.Builder as an argument
185+
186+
An `HtmxReponse.Builder` can be injected as a controller method. This creates the parameter and adds it to the model,
187+
allowing it to be used without requiring it be the method return value. This is useful when the return value is needed for
188+
the template.
189+
190+
This allows for the following usage:
191+
192+
```java
193+
@GetMapping("/endpoint")
194+
public String endpoint(HtmxResponse.Builder htmxResponse, Model model) {
195+
htmxResponse.trigger("event1");
196+
model.addAttribute("aField", "aValue");
197+
return "endpointTemplate";
198+
}
199+
```
200+
201+
For example the [JTE templating library](https://jte.gg/) supports statically typed templates and can be used like so:
202+
203+
```java
204+
@GetMapping("/endpoint")
205+
public JteModel endpoint(HtmxResponse.Builder htmxResponse) {
206+
htmxResponse.trigger("event1");
207+
String aField = "aValue";
208+
return templates.endpointTemplate(aField);
209+
}
210+
```
211+
212+
184213
### Error handlers
185214

186215
It is possible to use `HtmxResponse` as a return type from error handlers.

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,41 @@
1515

1616
import com.fasterxml.jackson.core.JsonProcessingException;
1717
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import org.springframework.web.servlet.ModelAndView;
19+
import org.springframework.web.servlet.View;
1820

1921
public class HtmxHandlerInterceptor implements HandlerInterceptor {
2022

2123
private final ObjectMapper objectMapper;
24+
private final HtmxResponseHandlerMethodReturnValueHandler htmxResponseHandlerMethodReturnValueHandler;
2225

23-
public HtmxHandlerInterceptor(ObjectMapper objectMapper) {
26+
public HtmxHandlerInterceptor(ObjectMapper objectMapper, HtmxResponseHandlerMethodReturnValueHandler htmxResponseHandlerMethodReturnValueHandler) {
2427
this.objectMapper = objectMapper;
28+
this.htmxResponseHandlerMethodReturnValueHandler = htmxResponseHandlerMethodReturnValueHandler;
29+
}
30+
31+
@Override
32+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
33+
if(modelAndView != null) {
34+
modelAndView.getModel().values().forEach(
35+
value ->{
36+
if(value instanceof HtmxResponse) {
37+
buildAndRender((HtmxResponse) value, modelAndView, request, response);
38+
} else if (value instanceof HtmxResponse.Builder) {
39+
buildAndRender(((HtmxResponse.Builder) value).build(), modelAndView, request, response);
40+
}
41+
});
42+
}
43+
}
44+
45+
private void buildAndRender(HtmxResponse htmxResponse, ModelAndView mav, HttpServletRequest request, HttpServletResponse response) {
46+
View v = htmxResponseHandlerMethodReturnValueHandler.toView(htmxResponse);
47+
try {
48+
v.render(mav.getModel(), request, response);
49+
htmxResponseHandlerMethodReturnValueHandler.addHxHeaders(htmxResponse, response);
50+
} catch (Exception e) {
51+
throw new RuntimeException(e);
52+
}
2553
}
2654

2755
@Override

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,21 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
4343

4444
@Override
4545
public void addInterceptors(InterceptorRegistry registry) {
46-
registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper));
46+
registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper, createHtmxReponseHandler()));
4747
}
4848

4949
@Override
5050
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
5151
resolvers.add(new HtmxHandlerMethodArgumentResolver());
52+
resolvers.add(new HtmxResponseHandlerMethodArgumentResolver());
5253
}
5354

5455
@Override
5556
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
56-
handlers.add(new HtmxResponseHandlerMethodReturnValueHandler(resolver.getObject(), locales, objectMapper));
57+
handlers.add(createHtmxReponseHandler());
58+
}
59+
60+
private HtmxResponseHandlerMethodReturnValueHandler createHtmxReponseHandler() {
61+
return new HtmxResponseHandlerMethodReturnValueHandler(resolver.getObject(), locales, objectMapper);
5762
}
5863
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.mvc;
2+
3+
import org.springframework.core.MethodParameter;
4+
import org.springframework.web.bind.support.WebDataBinderFactory;
5+
import org.springframework.web.context.request.NativeWebRequest;
6+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
7+
import org.springframework.web.method.support.ModelAndViewContainer;
8+
9+
public class HtmxResponseHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
10+
@Override
11+
public boolean supportsParameter(MethodParameter parameter) {
12+
return parameter.getParameterType().equals(HtmxResponse.Builder.class);
13+
}
14+
15+
@Override
16+
public Object resolveArgument(MethodParameter parameter,
17+
ModelAndViewContainer mavContainer,
18+
NativeWebRequest webRequest,
19+
WebDataBinderFactory binderFactory) throws Exception {
20+
HtmxResponse.Builder htmxResponseBuilder = HtmxResponse.builder();
21+
if(mavContainer != null) {
22+
mavContainer.addAttribute(htmxResponseBuilder);
23+
}
24+
return htmxResponseBuilder;
25+
}
26+
}

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public void handleReturnValue(Object returnValue,
5050
addHxHeaders(htmxResponse, webRequest.getNativeResponse(HttpServletResponse.class));
5151
}
5252

53-
private View toView(HtmxResponse htmxResponse) {
53+
View toView(HtmxResponse htmxResponse) {
5454

5555
Assert.notNull(htmxResponse, "HtmxResponse must not be null!");
5656

@@ -74,7 +74,7 @@ private View toView(HtmxResponse htmxResponse) {
7474
};
7575
}
7676

77-
private void addHxHeaders(HtmxResponse htmxResponse, HttpServletResponse response) {
77+
void addHxHeaders(HtmxResponse htmxResponse, HttpServletResponse response) {
7878
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal());
7979
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal());
8080
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal());

htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java

+8
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ public void throwException() {
137137
throw new RuntimeException("Fake exception");
138138
}
139139

140+
@GetMapping("/argument")
141+
public String argument(HtmxResponse.Builder htmxResponse) {
142+
htmxResponse.trigger("event1");
143+
return "argument";
144+
}
145+
146+
147+
140148
@ExceptionHandler(Exception.class)
141149
public HtmxResponse handleError(Exception ex) {
142150
return HtmxResponse.builder()

htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java

+7
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,11 @@ public void testException() throws Exception {
143143
<span>Fake exception</span>
144144
</span>""");
145145
}
146+
147+
@Test
148+
public void testHxTriggerArgument() throws Exception {
149+
mockMvc.perform(get("/hvhi/argument"))
150+
.andExpect(status().isOk())
151+
.andExpect(header().string("HX-Trigger", "event1"));
152+
}
146153
}

htmx-spring-boot/src/test/resources/templates/argument.html

Whitespace-only changes.

0 commit comments

Comments
 (0)