diff --git a/README.md b/README.md index ad38bdcc..cd9c9c85 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,24 @@ The following annotations are currently supported: >**Note** Please refer to the related Javadoc to learn more about the available options. -#### Examples +There are two ways to set htmx response headers on controller methods. +The first is to use annotations, e.g. `@HxTrigger`, and the second is to use the class [HtmxResponse](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.html) as the return type of the controller method. + +Here you can find a list of all available annotations: +* [@HxLocation](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.html) +* [@HxPushUrl](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.html) +* [@HxRedirect](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRedirect.html) +* [@HxRefresh](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRefresh.html) +* [@HxReplaceUrl](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReplaceUrl.html) +* [@HxReselect](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReselect.html) +* [@HxReswap](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReswap.html) +* [@HxRetarget](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRetarget.html) +* [@HxTrigger](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.html) -If you want htmx to trigger an event after the response is processed, you can use the annotation `@HxTrigger` which sets the necessary response header [HX-Trigger](https://htmx.org/headers/hx-trigger/). +>**Note** Please check the corresponding Javadoc to learn about the available options. +#### Examples +If you want htmx to trigger an event by setting the response header [HX-Trigger](https://htmx.org/headers/hx-trigger/), you can use the annotation `@HxTrigger`. ```java @HxRequest @HxTrigger("userUpdated") // the event 'userUpdated' will be triggered by htmx 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 7bf23b46..dc327416 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 @@ -1,27 +1,41 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_REFRESH; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_TRIGGER; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP; - -import java.lang.reflect.Method; - +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Nonnull; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpHeaders; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.time.Duration; + +import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*; public class HtmxHandlerInterceptor implements HandlerInterceptor { + + private final ObjectMapper objectMapper; + + public HtmxHandlerInterceptor(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) { + public boolean preHandle(@Nonnull HttpServletRequest request, + @Nonnull HttpServletResponse response, + @Nonnull Object handler) { if (handler instanceof HandlerMethod) { Method method = ((HandlerMethod) handler).getMethod(); + setHxLocation(response, method); + setHxPushUrl(response, method); + setHxRedirect(response, method); + setHxReplaceUrl(response, method); + setHxReswap(response, method); + setHxRetarget(response, method); + setHxReselect(response, method); setHxTrigger(response, method); setHxRefresh(response, method); setVary(request, response); @@ -30,6 +44,60 @@ public boolean preHandle(HttpServletRequest request, return true; } + private void setHxLocation(HttpServletResponse response, Method method) { + HxLocation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxLocation.class); + if (methodAnnotation != null) { + var location = convertToLocation(methodAnnotation); + if (location.hasContextData()) { + setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION.getValue(), location); + } else { + response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), location.getPath()); + } + } + } + + private void setHxPushUrl(HttpServletResponse response, Method method) { + HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class); + if (methodAnnotation != null) { + response.setHeader(HX_PUSH_URL.getValue(), methodAnnotation.value()); + } + } + + private void setHxRedirect(HttpServletResponse response, Method method) { + HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class); + if (methodAnnotation != null) { + response.setHeader(HX_REDIRECT.getValue(), methodAnnotation.value()); + } + } + + private void setHxReplaceUrl(HttpServletResponse response, Method method) { + HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class); + if (methodAnnotation != null) { + response.setHeader(HX_REPLACE_URL.getValue(), methodAnnotation.value()); + } + } + + private void setHxReswap(HttpServletResponse response, Method method) { + HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class); + if (methodAnnotation != null) { + response.setHeader(HX_RESWAP.getValue(), convertToReswap(methodAnnotation)); + } + } + + private void setHxRetarget(HttpServletResponse response, Method method) { + HxRetarget methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRetarget.class); + if (methodAnnotation != null) { + response.setHeader(HX_RETARGET.getValue(), methodAnnotation.value()); + } + } + + private void setHxReselect(HttpServletResponse response, Method method) { + HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class); + if (methodAnnotation != null) { + response.setHeader(HX_RESELECT.getValue(), methodAnnotation.value()); + } + } + private void setVary(HttpServletRequest request, HttpServletResponse response) { if (request.getHeader(HtmxRequestHeader.HX_REQUEST.getValue()) != null) { response.addHeader(HttpHeaders.VARY, HtmxRequestHeader.HX_REQUEST.getValue()); @@ -62,4 +130,73 @@ private String getHeaderName(HxTriggerLifecycle lifecycle) { throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle); } } + + private void setHeaderJsonValue(HttpServletResponse response, String name, Object value) { + try { + response.setHeader(name, objectMapper.writeValueAsString(value)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to set header " + name + " to " + value, e); + } + } + + private HtmxLocation convertToLocation(HxLocation annotation) { + var location = new HtmxLocation(); + location.setPath(annotation.path()); + if (!annotation.source().isEmpty()) { + location.setSource(annotation.source()); + } + if (!annotation.event().isEmpty()) { + location.setEvent(annotation.event()); + } + if (!annotation.handler().isEmpty()) { + location.setHandler(annotation.handler()); + } + if (!annotation.target().isEmpty()) { + location.setTarget(annotation.target()); + } + if (!annotation.target().isEmpty()) { + location.setSwap(annotation.swap()); + } + return location; + } + + private String convertToReswap(HxReswap annotation) { + + var reswap = new HtmxReswap(annotation.value()); + if (annotation.swap() != -1) { + reswap.swap(Duration.ofMillis(annotation.swap())); + } + if (annotation.settle() != -1) { + reswap.swap(Duration.ofMillis(annotation.settle())); + } + if (annotation.transition()) { + reswap.transition(); + } + if (annotation.focusScroll() != HxReswap.FocusScroll.UNDEFINED) { + reswap.focusScroll(annotation.focusScroll() == HxReswap.FocusScroll.TRUE); + } + if (annotation.show() != HxReswap.Position.UNDEFINED) { + reswap.show(convertToPosition(annotation.show())); + if (!annotation.showTarget().isEmpty()) { + reswap.scrollTarget(annotation.showTarget()); + } + } + if (annotation.scroll() != HxReswap.Position.UNDEFINED) { + reswap.scroll(convertToPosition(annotation.scroll())); + if (!annotation.scrollTarget().isEmpty()) { + reswap.scrollTarget(annotation.scrollTarget()); + } + } + + return reswap.toString(); + } + + private HtmxReswap.Position convertToPosition(HxReswap.Position position) { + return switch (position) { + case TOP -> HtmxReswap.Position.TOP; + case BOTTOM -> HtmxReswap.Position.BOTTOM; + default -> throw new IllegalStateException("Unexpected value: " + position); + }; + } + } 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 f11347a5..3524d651 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,7 +43,7 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new HtmxHandlerInterceptor()); + registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper)); } @Override diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxValue.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxValue.java new file mode 100644 index 00000000..36356d9d --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxValue.java @@ -0,0 +1,13 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +/** + * Holder for constant values. + */ +public class HtmxValue { + + /** + * Constant for use in annotations that support a {@code false} value to disable functions. + */ + public static final String FALSE = "false"; + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java new file mode 100644 index 00000000..b66db6b5 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java @@ -0,0 +1,56 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to do a client side redirect that does not do a full page reload. + *
+ * Note that this annotation does not provide support for specifying {@code values} or {@code headers}. + * If you want to do this, use {@link HtmxResponse} instead. + * + * @see HX-Location Response Header + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxLocation { + + /** + * The url path to make the redirect. + *

This is an alias for {@link #path}. For example, + * {@code @HxLocation("/foo")} is equivalent to + * {@code @HxLocation(path="/foo")}. + */ + @AliasFor("path") + String value() default ""; + /** + * The url path to make the redirect. + */ + String path() default ""; + /** + * The source element of the request + */ + String source() default ""; + /** + * An event that "triggered" the request + */ + String event() default ""; + /** + * A callback that will handle the response HTML. + */ + String handler() default ""; + /** + * The target to swap the response into. + */ + String target() default ""; + /** + * How the response will be swapped in relative to the target + */ + String swap() default ""; + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.java new file mode 100644 index 00000000..40cbd9a9 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.java @@ -0,0 +1,24 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to push a new url into the history stack. + * + * @see HX-Push-Url Response Header + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxPushUrl { + + /** + * The URL to be pushed into the location bar. This may be relative or absolute, + * as per history.pushState(). + * Or {@link HtmxValue#FALSE}, which prevents the browser’s history from being updated. + */ + String value(); + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRedirect.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRedirect.java new file mode 100644 index 00000000..4edf356a --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRedirect.java @@ -0,0 +1,22 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to do a client-side redirect to a new location. + * + * @see HX-Redirect + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxRedirect { + + /** + * The URL to use to do a client-side redirect to a new location. + */ + String value(); + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReplaceUrl.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReplaceUrl.java new file mode 100644 index 00000000..5085768f --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReplaceUrl.java @@ -0,0 +1,28 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to replace the current URL in the location bar. + * + * @see HX-Replace-Url + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxReplaceUrl { + + /** + * The URL to replace the current URL in the location bar. + * This may be relative or absolute, as per + * history.replaceState(), + * but must have the same origin as the current URL. + * Or {@link HtmxValue#FALSE} which prevents the browser’s current URL from being updated. + */ + String value(); + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReselect.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReselect.java new file mode 100644 index 00000000..08bc4769 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReselect.java @@ -0,0 +1,24 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to specify a CSS selector that allows you to choose which part + * of the response is used to be swapped in. + * + * @see HX-Retarget + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxReselect { + + /** + * A CSS selector that allows you to choose which part of the response is used to be swapped in. + *

Overrides an existing hx-select on the triggering element. + */ + String value(); + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReswap.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReswap.java new file mode 100644 index 00000000..ad2c1aeb --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReswap.java @@ -0,0 +1,87 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to specify how the response will be swapped. + * See hx-swap for possible values. + * + * @see HX-Reswap + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxReswap { + + /** + * A value to specify how the response will be swapped. + * + * @see hx-swap + */ + HxSwapType value() default HxSwapType.INNER_HTML; + + /** + * Set the time in milliseconds that should elapse after receiving a response to swap the content. + */ + long swap() default -1; + + /** + * Set the time in milliseconds that should elapse between the swap and the settle logic. + */ + long settle() default -1; + + /** + * Changes the scrolling behavior of the target element. + */ + Position scroll() default Position.UNDEFINED; + + /** + * Used to target a different element for scrolling. + */ + String scrollTarget() default ""; + + /** + * Changes the scrolling behavior of the target element. + */ + Position show() default Position.UNDEFINED; + + /** + * Used to target a different element for showing. + */ + String showTarget() default ""; + + /** + * Enables the use of the new + * View Transitions API + * when a swap occurs. + */ + boolean transition() default false; + + /** + * Enable or disable auto-scrolling to focused inputs between requests. + */ + FocusScroll focusScroll() default FocusScroll.UNDEFINED; + + /** + * Represents the values for {@link #focusScroll()} + */ + public enum FocusScroll { + TRUE, + FALSE, + UNDEFINED + } + + /** + * Represents the position values for {@link #show()} and {@link #scroll()} + */ + public enum Position { + TOP, + BOTTOM, + UNDEFINED + } + +} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRetarget.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRetarget.java new file mode 100644 index 00000000..1e85ec86 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRetarget.java @@ -0,0 +1,23 @@ +package io.github.wimdeblauwe.htmx.spring.boot.mvc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to specify a CSS selector that updates the target of + * the content update to a different element on the page. + * + * @see HX-Retarget + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HxRetarget { + + /** + * A CSS selector that updates the target of the content update to a different element on the page. + */ + String value(); + +} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java index 74b8e34c..ef98d93a 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java @@ -81,4 +81,61 @@ public void testHxRefresh() throws Exception { .andExpect(status().isOk()) .andExpect(header().string("HX-Refresh", "true")); } + + @Test + public void testHxLocationWithContextData() throws Exception { + mockMvc.perform(get("/hx-location-with-context-data")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Location", "{\"path\":\"/path\",\"source\":\"source\",\"event\":\"event\",\"handler\":\"handler\",\"target\":\"target\",\"swap\":\"swap\"}")); + } + + @Test + public void testHxLocationWithoutContextData() throws Exception { + mockMvc.perform(get("/hx-location-without-context-data")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Location", "/path")); + } + + @Test + public void testHxPushUrl() throws Exception { + mockMvc.perform(get("/hx-push-url")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Push-Url", "/path")); + } + + @Test + public void testHxRedirect() throws Exception { + mockMvc.perform(get("/hx-redirect")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Redirect", "/path")); + } + + @Test + public void testHxReplaceUrl() throws Exception { + mockMvc.perform(get("/hx-replace-url")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Replace-Url", "/path")); + } + + @Test + public void testHxReswap() throws Exception { + mockMvc.perform(get("/hx-reswap")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Reswap", "innerHTML swap:300ms")); + } + + @Test + public void testHxRetarget() throws Exception { + mockMvc.perform(get("/hx-retarget")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Retarget", "#target")); + } + + @Test + public void testHxReselect() throws Exception { + mockMvc.perform(get("/hx-reselect")) + .andExpect(status().isOk()) + .andExpect(header().string("HX-Reselect", "#target")); + } + } 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..f7a5d60e 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 @@ -138,7 +138,7 @@ public void testException() throws Exception { .andExpect(status().isOk()) .andExpect(header().string("HX-Reswap", "none")) .andReturn().getResponse().getContentAsString(); - assertThat(html).contains(""" + assertThat(html).containsIgnoringWhitespaces(""" Fake exception """); diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java index f4cc67ea..e0acd385 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java @@ -50,7 +50,6 @@ public String hxTriggerWithAliasForOverride() { return ""; } - @GetMapping("/hx-refresh") @HxRefresh @ResponseBody @@ -58,6 +57,62 @@ public String hxRefresh() { return ""; } + @GetMapping("/hx-location-without-context-data") + @HxLocation("/path") + @ResponseBody + public String hxLocationWithoutContextData() { + return ""; + } + + @GetMapping("/hx-location-with-context-data") + @HxLocation(path = "/path", source = "source", event = "event", handler = "handler", target = "target", swap = "swap") + @ResponseBody + public String hxLocationWithContextData() { + return ""; + } + + @GetMapping("/hx-push-url") + @HxPushUrl("/path") + @ResponseBody + public String hxPushUrl() { + return ""; + } + + @GetMapping("/hx-redirect") + @HxRedirect("/path") + @ResponseBody + public String hxRedirect() { + return ""; + } + + @GetMapping("/hx-replace-url") + @HxReplaceUrl("/path") + @ResponseBody + public String hxReplaceUrl() { + return ""; + } + + @GetMapping("/hx-reswap") + @HxReswap(value = HxSwapType.INNER_HTML, swap = 300) + @ResponseBody + public String hxReswap() { + return ""; + } + + @GetMapping("/hx-retarget") + @HxRetarget("#target") + @ResponseBody + public String hxRetarget() { + return ""; + } + + @GetMapping("/hx-reselect") + @HxReselect("#target") + @ResponseBody + public String hxReselect() { + return ""; + } + @GetMapping("/hx-vary") @ResponseBody public String hxVary() {