diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMapping.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMapping.java index c2e7ffd..77666d6 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMapping.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMapping.java @@ -1,13 +1,15 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringUtils; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; +import java.util.ArrayList; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.HX_REQUEST; +import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*; public class HtmxRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override @@ -24,7 +26,20 @@ protected RequestCondition getCustomMethodCondition(Method method) { private RequestCondition createCondition(HxRequest hxRequest) { if (hxRequest != null) { - return new HeadersRequestCondition(HX_REQUEST.getValue()); + var requestHeaders = new ArrayList(); + requestHeaders.add(HX_REQUEST.getValue()); + + if (StringUtils.hasText(hxRequest.target())) { + requestHeaders.add(HX_TARGET.getValue() + "=" + hxRequest.target()); + } + if (StringUtils.hasText(hxRequest.triggerId())) { + requestHeaders.add(HX_TRIGGER.getValue() + "=" + hxRequest.triggerId()); + } + if (StringUtils.hasText(hxRequest.triggerName())) { + requestHeaders.add(HX_TRIGGER_NAME.getValue() + "=" + hxRequest.triggerName()); + } + + return new HeadersRequestCondition(requestHeaders.toArray(String[]::new)); } return null; } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRequest.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRequest.java index 46195cb..8541954 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRequest.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRequest.java @@ -6,9 +6,31 @@ import java.lang.annotation.Target; /** - * Annotation for mapping htmx requests onto specific handler methods. + * Annotation for mapping htmx requests onto specific handler method. */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface HxRequest { + + /** + * Restricts the mapping to the {@code id} of a specific target element. + * + * @see HX-Target + */ + String target() default ""; + + /** + * Restricts the mapping to the {@code id} of a specific triggered element. + * + * @see HX-Trigger + */ + String triggerId() default ""; + + /** + * Restricts the mapping to the {@code name} of a specific triggered element. + * + * @see HX-Trigger-Name + */ + String triggerName() default ""; + } diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java new file mode 100644 index 0000000..5b6aa46 --- /dev/null +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java @@ -0,0 +1,78 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HtmxRequestMappingHandlerMappingTestController.class) +@WithMockUser +public class HtmxRequestMappingHandlerMappingTest { + + @Autowired + private MockMvc mockMvc; + @MockBean + private TestService service; + + @Test + void testHxRequestWithTargetBar() throws Exception { + mockMvc.perform(get("/hx-request-with-target") + .header(HX_REQUEST.getValue(), "true") + .header(HX_TARGET.getValue(), "bar")) + .andExpect(status().isOk()) + .andExpect(content().string("bar")); + } + + @Test + void testHxRequestWithTargetFoo() throws Exception { + mockMvc.perform(get("/hx-request-with-target") + .header(HX_REQUEST.getValue(), "true") + .header(HX_TARGET.getValue(), "foo")) + .andExpect(status().isOk()) + .andExpect(content().string("foo")); + } + + @Test + void testHxRequestWithTriggerIdBar() throws Exception { + mockMvc.perform(get("/hx-request-with-trigger-id") + .header(HX_REQUEST.getValue(), "true") + .header(HX_TRIGGER.getValue(), "bar")) + .andExpect(status().isOk()) + .andExpect(content().string("bar")); + } + + @Test + void testHxRequestWithTriggerIdFoo() throws Exception { + mockMvc.perform(get("/hx-request-with-trigger-id") + .header(HX_REQUEST.getValue(), "true") + .header(HX_TRIGGER.getValue(), "foo")) + .andExpect(status().isOk()) + .andExpect(content().string("foo")); + } + + @Test + void testHxRequestWithTriggerNameBar() throws Exception { + mockMvc.perform(get("/hx-request-with-trigger-name") + .header(HX_REQUEST.getValue(), "true") + .header(HX_TRIGGER_NAME.getValue(), "bar")) + .andExpect(status().isOk()) + .andExpect(content().string("bar")); + } + + @Test + void testHxRequestWithTriggerNameFoo() throws Exception { + mockMvc.perform(get("/hx-request-with-trigger-name") + .header(HX_REQUEST.getValue(), "true") + .header(HX_TRIGGER_NAME.getValue(), "foo")) + .andExpect(status().isOk()) + .andExpect(content().string("foo")); + } + +} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java new file mode 100644 index 0000000..4847175 --- /dev/null +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java @@ -0,0 +1,52 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class HtmxRequestMappingHandlerMappingTestController { + + @HxRequest(target = "bar") + @GetMapping("/hx-request-with-target") + @ResponseBody + public String hxRequestWithTargetBar() { + return "bar"; + } + + @HxRequest(target = "foo") + @GetMapping("/hx-request-with-target") + @ResponseBody + public String hxRequestWithTargetFoo() { + return "foo"; + } + + @HxRequest(triggerId = "bar") + @GetMapping("/hx-request-with-trigger-id") + @ResponseBody + public String hxRequestWithTriggerIdBar() { + return "bar"; + } + + @HxRequest(triggerId = "foo") + @GetMapping("/hx-request-with-trigger-id") + @ResponseBody + public String hxRequestWithTriggerIdFoo() { + return "foo"; + } + + @HxRequest(triggerName = "bar") + @GetMapping("/hx-request-with-trigger-name") + @ResponseBody + public String hxRequestWithTriggerNameBar() { + return "bar"; + } + + @HxRequest(triggerName = "foo") + @GetMapping("/hx-request-with-trigger-name") + @ResponseBody + public String hxRequestWithTriggerNameFoo() { + return "foo"; + } + +}