Skip to content

Commit 6bcf2c7

Browse files
committed
Merge pull request #183 from fchtngr/main
Handle htmx headers for @responsebody methods
2 parents b732b3c + 806cb69 commit 6bcf2c7

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
1313
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
1414
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
15+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
1516
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
1617
import tools.jackson.databind.json.JsonMapper;
1718

@@ -22,17 +23,26 @@
2223
public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConfigurer {
2324

2425
private final HtmxHandlerMethodHandler handlerMethodHandler;
26+
private final HtmxResponseBodyAdvice responseBodyAdvice;
2527

2628
HtmxMvcAutoConfiguration() {
2729
JsonMapper jsonMapper = JsonMapper.builder().build();
2830
this.handlerMethodHandler = new HtmxHandlerMethodHandler(jsonMapper);
31+
this.responseBodyAdvice = new HtmxResponseBodyAdvice(handlerMethodHandler);
2932
}
3033

3134
@Override
3235
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
3336
return new HtmxRequestMappingHandlerMapping();
3437
}
3538

39+
@Override
40+
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
41+
var adapter = new RequestMappingHandlerAdapter();
42+
adapter.setResponseBodyAdvice(List.of(responseBodyAdvice));
43+
return adapter;
44+
}
45+
3646
@Override
3747
public void addInterceptors(InterceptorRegistry registry) {
3848
registry.addInterceptor(new HtmxHandlerInterceptor(handlerMethodHandler));
@@ -46,7 +56,9 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
4656

4757
@Override
4858
public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
49-
return new HtmxExceptionHandlerExceptionResolver(handlerMethodHandler);
59+
var resolver = new HtmxExceptionHandlerExceptionResolver(handlerMethodHandler);
60+
resolver.setResponseBodyAdvice(List.of(responseBodyAdvice));
61+
return resolver;
5062
}
5163

5264
@Bean
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.mvc;
2+
3+
import org.springframework.core.MethodParameter;
4+
import org.springframework.http.MediaType;
5+
import org.springframework.http.converter.HttpMessageConverter;
6+
import org.springframework.http.server.ServerHttpRequest;
7+
import org.springframework.http.server.ServerHttpResponse;
8+
import org.springframework.http.server.ServletServerHttpRequest;
9+
import org.springframework.http.server.ServletServerHttpResponse;
10+
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
11+
12+
/**
13+
* A {@link ResponseBodyAdvice} implementation that adds htmx response headers
14+
* based on {@link HtmxResponse}.
15+
*
16+
* @since 5.0.0
17+
*/
18+
public class HtmxResponseBodyAdvice implements ResponseBodyAdvice<Object> {
19+
20+
private final HtmxHandlerMethodHandler htmxHandlerMethodHandler;
21+
22+
public HtmxResponseBodyAdvice(HtmxHandlerMethodHandler htmxHandlerMethodHandler) {
23+
this.htmxHandlerMethodHandler = htmxHandlerMethodHandler;
24+
}
25+
26+
@Override
27+
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
28+
return true; // Apply to all @ResponseBody methods
29+
}
30+
31+
@Override
32+
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
33+
Class<? extends HttpMessageConverter<?>> selectedConverterType,
34+
ServerHttpRequest request, ServerHttpResponse response) {
35+
36+
if (request instanceof ServletServerHttpRequest servletRequest
37+
&& response instanceof ServletServerHttpResponse servletResponse) {
38+
htmxHandlerMethodHandler.handleMethodArgument(servletRequest.getServletRequest(),
39+
servletResponse.getServletResponse());
40+
}
41+
return body;
42+
}
43+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.mvc;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
6+
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.context.annotation.Import;
8+
import org.springframework.test.web.servlet.client.RestTestClient;
9+
import org.springframework.web.bind.annotation.ExceptionHandler;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
14+
@Import({ HtmxResponseBodyAdviceIT.TestRestController.class })
15+
@AutoConfigureRestTestClient
16+
public class HtmxResponseBodyAdviceIT {
17+
18+
@Autowired
19+
RestTestClient webClient;
20+
21+
@Test
22+
public void testTrigger() throws Exception {
23+
24+
get("/trigger")
25+
.expectHeader()
26+
.valueEquals("HX-Trigger", "trigger1,trigger2");
27+
}
28+
29+
@Test
30+
public void testExceptionHandler() throws Exception {
31+
32+
get("/throw-exception")
33+
.expectHeader()
34+
.valueEquals("HX-Retarget", "#container");
35+
}
36+
37+
private RestTestClient.ResponseSpec get(String uri) {
38+
39+
return webClient
40+
.get()
41+
.uri(uri)
42+
.exchange()
43+
.expectStatus()
44+
.isOk();
45+
}
46+
47+
@RestController
48+
static class TestRestController {
49+
50+
@GetMapping("/trigger")
51+
public Object triggerResponseBody(HtmxResponse response) {
52+
53+
response.addTrigger("trigger1");
54+
response.addTrigger("trigger2");
55+
return "response";
56+
}
57+
58+
@GetMapping("/throw-exception")
59+
public Object throwException() {
60+
61+
throw new RuntimeException();
62+
}
63+
64+
@ExceptionHandler(RuntimeException.class)
65+
public Object handleError(RuntimeException ex, HtmxResponse htmxResponse) {
66+
67+
htmxResponse.setRetarget("#container");
68+
return "view";
69+
}
70+
71+
}
72+
73+
}

0 commit comments

Comments
 (0)