From ea0bf6854772a5181f734ecf906503444ce8b68a Mon Sep 17 00:00:00 2001
From: Peter Fokkinga
Date: Fri, 19 Apr 2024 06:58:02 +0200
Subject: [PATCH] feat: new "morph" and "refresh" action types for
turbo-stream, issue #772 (#773)
---
.../io/micronaut/views/turbo/TurboStream.java | 97 +++++++++++++++++++
.../views/turbo/TurboStreamAction.java | 12 ++-
.../io/micronaut/views/turbo/TurboView.java | 12 +++
.../views/turbo/TurboStreamSpec.groovy | 38 +++++++-
4 files changed, 157 insertions(+), 2 deletions(-)
diff --git a/views-core/src/main/java/io/micronaut/views/turbo/TurboStream.java b/views-core/src/main/java/io/micronaut/views/turbo/TurboStream.java
index 8c50b5957..0aa2663d2 100644
--- a/views-core/src/main/java/io/micronaut/views/turbo/TurboStream.java
+++ b/views-core/src/main/java/io/micronaut/views/turbo/TurboStream.java
@@ -41,12 +41,16 @@ public final class TurboStream implements Renderable {
private static final String MEMBER_ACTION = "action";
private static final String MEMBER_TARGET_DOM_ID = "targetDomId";
private static final String MEMBER_TARGET_CSS_QUERY_SELECTOR = "targetCssQuerySelector";
+ private static final String MEMBER_REQUEST_ID = "requestId";
+ private static final String MEMBER_CHILDREN_ONLY = "childrenOnly";
private static final String TURBO_TEMPLATE_TAG = "template";
private static final String TURBO_STREAM_TAG = "turbo-stream";
private static final String TURBO_STREAM_CLOSING_TAG = "";
private static final String TURBO_STREAM_ATTRIBUTE_TARGET = "target";
private static final String TURBO_STREAM_ATTRIBUTE_ACTION = "action";
private static final String TURBO_STREAM_ATTRIBUTE_TARGETS = "targets";
+ private static final String TURBO_STREAM_ATTRIBUTE_REQUEST_ID = "request-id";
+ private static final String TURBO_STREAM_ATTRIBUTE_CHILDREN_ONLY = "children-only";
private static final String CLOSE_TAG = ">";
private static final String OPEN_TAG = "<";
private static final String SPACE = " ";
@@ -69,9 +73,26 @@ public final class TurboStream implements Renderable {
@Nullable
private final String targetCssQuerySelector;
+ /**
+ * request-id attribute, only relevant when action=refresh
+ */
+ @Nullable
+ private final String requestId;
+
+ /**
+ * Morph only the children of the element designated by the target dom id.
+ */
+ private final boolean childrenOnly;
+
@Nullable
private final Object template;
+ /**
+ *
+ * @deprecated use the constructor that takes also the {@code requestId} and
+ * {@code childrenOnly} parameters instead
+ */
+ @Deprecated
TurboStream(@NonNull TurboStreamAction action,
@Nullable String targetDomId,
@Nullable String targetCssQuerySelector,
@@ -79,6 +100,22 @@ public final class TurboStream implements Renderable {
this.action = action;
this.targetDomId = targetDomId;
this.targetCssQuerySelector = targetCssQuerySelector;
+ this.requestId = null;
+ this.childrenOnly = false;
+ this.template = template;
+ }
+
+ TurboStream(@NonNull TurboStreamAction action,
+ @Nullable String targetDomId,
+ @Nullable String targetCssQuerySelector,
+ @Nullable String requestId,
+ boolean childrenOnly,
+ @Nullable Object template) {
+ this.action = action;
+ this.targetDomId = targetDomId;
+ this.targetCssQuerySelector = targetCssQuerySelector;
+ this.requestId = requestId;
+ this.childrenOnly = childrenOnly;
this.template = template;
}
@@ -109,6 +146,22 @@ public Optional getTargetCssQuerySelector() {
return Optional.ofNullable(targetCssQuerySelector);
}
+ /**
+ *
+ * @return request-id attribute, only relevant when action=refresh
+ */
+ public Optional getRequestId() {
+ return Optional.ofNullable(requestId);
+ }
+
+ /**
+ *
+ * @return Morph only the children of the element designated by the target dom id.
+ */
+ public boolean getChildrenOnly() {
+ return childrenOnly;
+ }
+
/**
*
* @return Template.
@@ -164,6 +217,8 @@ private String renderTurboStreamOpeningTag() {
return OPEN_TAG + TURBO_STREAM_TAG + SPACE + htmlAttribute(TURBO_STREAM_ATTRIBUTE_ACTION, getAction().toString())
+ getTargetDomIdHtmlAttribute().orElse("")
+ getTargetCssQuerySelectorHtmlAttribute().orElse("")
+ + getRequestIdAttribute().orElse("")
+ + getChildrenOnlyAttribute().orElse("")
+ CLOSE_TAG;
}
@@ -179,6 +234,19 @@ private Optional getTargetCssQuerySelectorHtmlAttribute() {
.map(cssSelector -> SPACE + htmlAttribute(TURBO_STREAM_ATTRIBUTE_TARGETS, cssSelector));
}
+ @NonNull
+ private Optional getRequestIdAttribute() {
+ return getRequestId()
+ .map(requestId -> SPACE + htmlAttribute(TURBO_STREAM_ATTRIBUTE_REQUEST_ID, requestId));
+ }
+
+ @NonNull
+ private Optional getChildrenOnlyAttribute() {
+ return getChildrenOnly()
+ ? Optional.of(SPACE + TURBO_STREAM_ATTRIBUTE_CHILDREN_ONLY)
+ : Optional.empty();
+ }
+
@NonNull
private String htmlAttribute(@NonNull String key, @NonNull String value) {
return String.join(EQUALS, key, DOUBLE_QUOTE + value + DOUBLE_QUOTE);
@@ -208,6 +276,8 @@ public static class Builder {
private TurboStreamAction action;
private String targetDomId;
private String targetCssQuerySelector;
+ private String requestId;
+ private boolean childrenOnly;
private Object template;
private String templateView;
private Object templateModel;
@@ -273,6 +343,29 @@ public Builder targetCssQuerySelector(@NonNull String targetCssQuerySelector) {
return this;
}
+
+ /**
+ *
+ * @param requestId request-id attribute, only relevant when action=refresh
+ * @return The Builder
+ */
+ @NonNull
+ public Builder requestId(@NonNull String requestId) {
+ this.requestId = requestId;
+ return this;
+ }
+
+ /**
+ *
+ * @param childrenOnly Morph only the children of the element designated by the target dom id.
+ * @return The Builder
+ */
+ @NonNull
+ public Builder childrenOnly(boolean childrenOnly) {
+ this.childrenOnly = childrenOnly;
+ return this;
+ }
+
/**
* Sets the template with a View and Model.
* @param view The View name
@@ -409,6 +502,8 @@ public TurboStream build() {
return new TurboStream(action,
targetDomId,
targetCssQuerySelector,
+ requestId,
+ childrenOnly,
template);
}
@@ -473,6 +568,8 @@ private static Optional of(@NonNull HttpRequest> request,
route.getValue(TurboView.class, MEMBER_ACTION, TurboStreamAction.class).ifPresent(builder::action);
route.stringValue(TurboView.class, MEMBER_TARGET_DOM_ID).ifPresent(builder::targetDomId);
route.stringValue(TurboView.class, MEMBER_TARGET_CSS_QUERY_SELECTOR).ifPresent(builder::targetCssQuerySelector);
+ route.stringValue(TurboView.class, MEMBER_REQUEST_ID).ifPresent(builder::requestId);
+ route.booleanValue(TurboView.class, MEMBER_CHILDREN_ONLY).ifPresent(builder::childrenOnly);
if (!builder.getTargetCssQuerySelector().isPresent() &&
!builder.getTargetDomId().isPresent()) {
diff --git a/views-core/src/main/java/io/micronaut/views/turbo/TurboStreamAction.java b/views-core/src/main/java/io/micronaut/views/turbo/TurboStreamAction.java
index dbb499891..2df2fe8d0 100644
--- a/views-core/src/main/java/io/micronaut/views/turbo/TurboStreamAction.java
+++ b/views-core/src/main/java/io/micronaut/views/turbo/TurboStreamAction.java
@@ -57,7 +57,17 @@ public enum TurboStreamAction {
/**
* Inserts the content within the template tag after the element designated by the target dom id.
*/
- AFTER("after");
+ AFTER("after"),
+
+ /**
+ * Replaces the element designated by the target dom id via morph.
+ */
+ MORPH("morph"),
+
+ /**
+ * Initiates a Page Refresh to render new content with morphing.
+ */
+ REFRESH("refresh");
@NonNull
private final String action;
diff --git a/views-core/src/main/java/io/micronaut/views/turbo/TurboView.java b/views-core/src/main/java/io/micronaut/views/turbo/TurboView.java
index 36b11cdc5..e79b2eb1e 100644
--- a/views-core/src/main/java/io/micronaut/views/turbo/TurboView.java
+++ b/views-core/src/main/java/io/micronaut/views/turbo/TurboView.java
@@ -55,4 +55,16 @@
* @return Target CSS Query Selector.
*/
String targetCssQuerySelector() default "";
+
+ /**
+ *
+ * @return request-id attribute, only relevant when action=refresh
+ */
+ String requestId() default "";
+
+ /**
+ *
+ * @return morph only the children of the element designated by the target dom id
+ */
+ boolean childrenOnly() default false;
}
diff --git a/views-core/src/test/groovy/io/micronaut/views/turbo/TurboStreamSpec.groovy b/views-core/src/test/groovy/io/micronaut/views/turbo/TurboStreamSpec.groovy
index 5a0ae8473..d80cd188c 100644
--- a/views-core/src/test/groovy/io/micronaut/views/turbo/TurboStreamSpec.groovy
+++ b/views-core/src/test/groovy/io/micronaut/views/turbo/TurboStreamSpec.groovy
@@ -155,6 +155,28 @@ class TurboStreamSpec extends Specification {
"Hello World
" == html
}
+ void "verify TurboView annotation can specify requestId"() {
+ given:
+ BlockingHttpClient client = httpClient.toBlocking()
+
+ when:
+ String html = client.retrieve(HttpRequest.GET("/turbo/requestId").accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML))
+
+ then:
+ "Hello World
" == html
+ }
+
+ void "verify TurboView annotation can specify childrenOnly"() {
+ given:
+ BlockingHttpClient client = httpClient.toBlocking()
+
+ when:
+ String html = client.retrieve(HttpRequest.GET("/turbo/childrenOnly").accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML))
+
+ then:
+ "Hello World
" == html
+ }
+
void "verify TurboView annotation can specify targetCssQuerySelector"() {
given:
BlockingHttpClient client = httpClient.toBlocking()
@@ -409,7 +431,7 @@ class TurboStreamSpec extends Specification {
@TurboView("fragments/message")
@Get("/update")
String update() {
- "Hello World";
+ "Hello World"
}
@Produces(TurboMediaType.TURBO_STREAM)
@@ -419,6 +441,20 @@ class TurboStreamSpec extends Specification {
"Hello World"
}
+ @Produces(TurboMediaType.TURBO_STREAM)
+ @TurboView(value = "fragments/message", action=TurboStreamAction.REFRESH, requestId = "abcd-1234")
+ @Get("/requestId")
+ String requestId() {
+ "Hello World"
+ }
+
+ @Produces(TurboMediaType.TURBO_STREAM)
+ @TurboView(value = "fragments/message", action=TurboStreamAction.MORPH, childrenOnly = true)
+ @Get("/childrenOnly")
+ String childrenOnly() {
+ "Hello World"
+ }
+
@Produces(TurboMediaType.TURBO_STREAM)
@TurboView(action = TurboStreamAction.REMOVE)
@Get("/del")