diff --git a/README.md b/README.md index ad38bdcc..ab247590 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,17 @@ The first is to use annotations, e.g. `@HxTrigger`, and the second is to use the See [Response Headers Reference](https://htmx.org/reference/#response_headers) for the related htmx documentation. The following annotations are currently supported: +* [@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) +* [@HxTriggerAfterSettle](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSettle.html) +* [@HxTriggerAfterSwap](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSwap.html) >**Note** Please refer to the related Javadoc to learn more about the available options. 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..c393a03d 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,11 +1,9 @@ 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 static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*; import java.lang.reflect.Method; +import java.time.Duration; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpHeaders; @@ -15,14 +13,34 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + 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) { + HttpServletResponse response, + 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); + setHxTriggerAfterSettle(response, method); + setHxTriggerAfterSwap(response, method); setHxRefresh(response, method); setVary(request, response); } @@ -36,30 +54,175 @@ private void setVary(HttpServletRequest request, HttpServletResponse response) { } } + 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, location); + } else { + setHeader(response, HtmxResponseHeader.HX_LOCATION, location.getPath()); + } + } + } + + private void setHxPushUrl(HttpServletResponse response, Method method) { + HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class); + if (methodAnnotation != null) { + setHeader(response, HX_PUSH_URL, methodAnnotation.value()); + } + } + + private void setHxRedirect(HttpServletResponse response, Method method) { + HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class); + if (methodAnnotation != null) { + setHeader(response, HX_REDIRECT, methodAnnotation.value()); + } + } + + private void setHxReplaceUrl(HttpServletResponse response, Method method) { + HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class); + if (methodAnnotation != null) { + setHeader(response, HX_REPLACE_URL, methodAnnotation.value()); + } + } + + private void setHxReswap(HttpServletResponse response, Method method) { + HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class); + if (methodAnnotation != null) { + setHeader(response, HX_RESWAP, convertToReswap(methodAnnotation)); + } + } + + private void setHxRetarget(HttpServletResponse response, Method method) { + HxRetarget methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRetarget.class); + if (methodAnnotation != null) { + setHeader(response, HX_RETARGET, methodAnnotation.value()); + } + } + + private void setHxReselect(HttpServletResponse response, Method method) { + HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class); + if (methodAnnotation != null) { + setHeader(response, HX_RESELECT, methodAnnotation.value()); + } + } + private void setHxTrigger(HttpServletResponse response, Method method) { HxTrigger methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTrigger.class); if (methodAnnotation != null) { - response.setHeader(getHeaderName(methodAnnotation.lifecycle()), methodAnnotation.value()); + setHeader(response, convertToHeader(methodAnnotation.lifecycle()), methodAnnotation.value()); + } + } + + private void setHxTriggerAfterSettle(HttpServletResponse response, Method method) { + HxTriggerAfterSettle methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTriggerAfterSettle.class); + if (methodAnnotation != null) { + setHeader(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, methodAnnotation.value()); + } + } + + private void setHxTriggerAfterSwap(HttpServletResponse response, Method method) { + HxTriggerAfterSwap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTriggerAfterSwap.class); + if (methodAnnotation != null) { + setHeader(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, methodAnnotation.value()); } } private void setHxRefresh(HttpServletResponse response, Method method) { HxRefresh methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRefresh.class); if (methodAnnotation != null) { - response.setHeader(HX_REFRESH.getValue(), "true"); + setHeader(response, HX_REFRESH, HtmxValue.TRUE); } } - private String getHeaderName(HxTriggerLifecycle lifecycle) { + private HtmxResponseHeader convertToHeader(HxTriggerLifecycle lifecycle) { switch (lifecycle) { case RECEIVE: - return HX_TRIGGER.getValue(); + return HX_TRIGGER; case SETTLE: - return HX_TRIGGER_AFTER_SETTLE.getValue(); + return HX_TRIGGER_AFTER_SETTLE; case SWAP: - return HX_TRIGGER_AFTER_SWAP.getValue(); + return HX_TRIGGER_AFTER_SWAP; default: throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle); } } + + private void setHeaderJsonValue(HttpServletResponse response, HtmxResponseHeader header, Object value) { + try { + response.setHeader(header.getValue(), objectMapper.writeValueAsString(value)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to set header " + header.getValue() + " to " + value, e); + } + } + + private void setHeader(HttpServletResponse response, HtmxResponseHeader header, String value) { + response.setHeader(header.getValue(), value); + } + + private void setHeader(HttpServletResponse response, HtmxResponseHeader header, String[] values) { + response.setHeader(header.getValue(), String.join(",", values)); + } + + 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..38f8a56a --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxValue.java @@ -0,0 +1,18 @@ +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"; + + /** + * Constant for use in annotations that support a {@code true} value to enable functions. + */ + public static final String TRUE = "true"; + +} 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..ef234108 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java @@ -0,0 +1,55 @@ +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 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..9af108e3 --- /dev/null +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.java @@ -0,0 +1,31 @@ +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 URL into the browser + * location history. + *
+ * The possible values are: + *
+ * The possible values are: + *
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..813ee4a2
--- /dev/null
+++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReswap.java
@@ -0,0 +1,85 @@
+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 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/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java
index 57428e84..a0931120 100644
--- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java
+++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java
@@ -6,14 +6,24 @@
import java.lang.annotation.Target;
/**
- * Annotation to trigger client side actions on the target element within a response to htmx.
+ * Annotation to trigger client side events as soon as the response is received on the target element.
+ *
+ * You can trigger a single event or as many uniquely named events as you would like.
*
* @see HX-Trigger Response Headers
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HxTrigger {
- String value();
+ /**
+ * The events to trigger as soon as the response is received on the target element.
+ */
+ String[] value();
+
+ /**
+ * @deprecated use annotation {@link HxTriggerAfterSettle} or {@link HxTriggerAfterSwap} instead.
+ */
+ @Deprecated
HxTriggerLifecycle lifecycle() default HxTriggerLifecycle.RECEIVE;
}
diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSettle.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSettle.java
new file mode 100644
index 00000000..bd44d208
--- /dev/null
+++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSettle.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;
+
+/**
+ * Annotation to trigger client side events after the
+ * settling step
+ * on the target element.
+ *
+ * You can trigger a single event or as many uniquely named events as you would like.
+ *
+ * @see HX-Trigger Response Headers
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface HxTriggerAfterSettle {
+
+ /**
+ * The events to trigger after the
+ * settling step
+ * on the target element.
+ */
+ String[] value();
+
+}
diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSwap.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSwap.java
new file mode 100644
index 00000000..3d1c511d
--- /dev/null
+++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSwap.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;
+
+/**
+ * Annotation to trigger client side events after the
+ * swap step
+ * on the target element.
+ *
+ * You can trigger a single event or as many uniquely named events as you would like.
+ *
+ * @see HX-Trigger Response Headers
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface HxTriggerAfterSwap {
+
+ /**
+ * The events to trigger after the
+ * swap step
+ * on the target element.
+ */
+ String[] value();
+
+}
diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java
index 775a2d47..418de349 100644
--- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java
+++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java
@@ -4,7 +4,9 @@
* Represents the HX-Trigger Response Headers.
*
* @see HX-Trigger Response Headers
+ * @deprecated use annotation {@link HxTriggerAfterSettle} or {@link HxTriggerAfterSwap} instead.
*/
+@Deprecated
public enum HxTriggerLifecycle {
/**
* Trigger events as soon as the response is received.
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..d772088e 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
@@ -26,6 +26,13 @@ public void testHeaderIsSetOnResponseIfHxTriggerIsPresent() throws Exception {
.andExpect(header().string("HX-Trigger", "eventTriggered"));
}
+ @Test
+ public void testHeaderIsSetOnResponseWithMultipleEventsIfHxTriggerIsPresent() throws Exception {
+ mockMvc.perform(get("/with-trigger-multiple-events"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("HX-Trigger", "event1,event2"));
+ }
+
@Test
public void testAfterSettleHeaderIsSetOnResponseIfHxTriggerIsPresent() throws Exception {
mockMvc.perform(get("/with-trigger-settle"))
@@ -40,6 +47,43 @@ public void testAfterSwapHeaderIsSetOnResponseIfHxTriggerIsPresent() throws Exce
.andExpect(header().string("HX-Trigger-After-Swap", "eventTriggered"));
}
+ @Test
+ public void testAfterSettleHeaderIsSetOnResponseIfHxTriggerAfterSettleIsPresent() throws Exception {
+ mockMvc.perform(get("/with-trigger-after-settle"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("HX-Trigger-After-Settle", "eventTriggered"));
+ }
+
+ @Test
+ public void testAfterSettleHeaderIsSetOnResponseWithMultipleEventsIfHxTriggerAfterSettleIsPresent() throws Exception {
+ mockMvc.perform(get("/with-trigger-after-settle-multiple-events"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("HX-Trigger-After-Settle", "event1,event2"));
+ }
+
+ @Test
+ public void testAfterSwapHeaderIsSetOnResponseIfHxTriggerAfterSwapIsPresent() throws Exception {
+ mockMvc.perform(get("/with-trigger-after-swap"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("HX-Trigger-After-Swap", "eventTriggered"));
+ }
+
+ @Test
+ public void testAfterSwapHeaderIsSetOnResponseWithMultipleEventsIfHxTriggerAfterSwapIsPresent() throws Exception {
+ mockMvc.perform(get("/with-trigger-after-swap-multiple-events"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("HX-Trigger-After-Swap", "event1,event2"));
+ }
+
+ @Test
+ public void testHeadersAreSetOnResponseIfHxTriggersArePresent() throws Exception {
+ mockMvc.perform(get("/with-triggers"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("HX-Trigger", "event1,event2"))
+ .andExpect(header().string("HX-Trigger-After-Settle", "event1,event2"))
+ .andExpect(header().string("HX-Trigger-After-Swap", "event1,event2"));
+ }
+
@Test
public void testHeaderIsNotSetOnResponseIfHxTriggerNotPresent() throws Exception {
mockMvc.perform(get("/without-trigger"))
@@ -81,4 +125,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/TestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java
index f4cc67ea..85bfbc84 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
@@ -22,20 +22,64 @@ public String methodWithHxTrigger() {
return "";
}
+ @GetMapping("/with-trigger-multiple-events")
+ @HxTrigger({ "event1", "event2" })
+ @ResponseBody
+ public String methodWithHxTriggerAndMultipleEvents() {
+ return "";
+ }
+
@GetMapping("/with-trigger-settle")
@HxTrigger(value = "eventTriggered", lifecycle = HxTriggerLifecycle.SETTLE)
@ResponseBody
- public String methodWithHxTriggerAfterSettle() {
+ public String methodWithHxTriggerAndLifecycleSettle() {
return "";
}
@GetMapping("/with-trigger-swap")
@HxTrigger(value = "eventTriggered", lifecycle = HxTriggerLifecycle.SWAP)
@ResponseBody
+ public String methodWithHxTriggerAndLifecycleSwap() {
+ return "";
+ }
+
+ @GetMapping("/with-trigger-after-settle")
+ @HxTriggerAfterSettle("eventTriggered")
+ @ResponseBody
+ public String methodWithHxTriggerAfterSettle() {
+ return "";
+ }
+
+ @GetMapping("/with-trigger-after-settle-multiple-events")
+ @HxTriggerAfterSettle({ "event1", "event2" })
+ @ResponseBody
+ public String methodWithHxTriggerAfterSettleAndMultipleEvents() {
+ return "";
+ }
+
+ @GetMapping("/with-trigger-after-swap")
+ @HxTriggerAfterSwap("eventTriggered")
+ @ResponseBody
public String methodWithHxTriggerAfterSwap() {
return "";
}
+ @GetMapping("/with-trigger-after-swap-multiple-events")
+ @HxTriggerAfterSwap({ "event1", "event2" })
+ @ResponseBody
+ public String methodWithHxTriggerAfterSwapAndMultipleEvents() {
+ return "";
+ }
+
+ @GetMapping("/with-triggers")
+ @HxTrigger({ "event1", "event2" })
+ @HxTriggerAfterSettle({ "event1", "event2" })
+ @HxTriggerAfterSwap({ "event1", "event2" })
+ @ResponseBody
+ public String methodWithHxTriggers() {
+ return "";
+ }
+
@GetMapping("/updates-sidebar")
@HxUpdatesSidebar
@ResponseBody
@@ -50,7 +94,6 @@ public String hxTriggerWithAliasForOverride() {
return "";
}
-
@GetMapping("/hx-refresh")
@HxRefresh
@ResponseBody
@@ -64,4 +107,60 @@ public String hxVary() {
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 "";
+ }
+
}