Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Qute] Allow nested content for user defined tags #10652

Merged
merged 2 commits into from
Jul 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,12 @@ Let's suppose we have a template called `itemDetail.html`:
----
{#if showImage} <1>
{it.image} <2>
{nested-content} <3>
{/if}
----
<1> `showImage` is a named parameter.
<2> `it` is a special key that is replaced with the first unnamed param of the tag.
<3> (optional) `nested-content` is a special key that will be replaced by the content of the tag.

Now if we register this template under the name `itemDetail.html` and if we add a `UserTagSectionHelper` to the engine:

Expand All @@ -656,12 +658,15 @@ We can include the tag like this:
<ul>
{#for item in items}
<li>
{#itemDetail item showImage=true /} <1>
{#itemDetail item showImage=true} <1>
= <b>{item.name}</b> <2>
{/itemDetail}
</li>
{/for}
</ul>
----
<1> `item` is resolved to an iteration element and can be referenced using the `it` key in the tag template.
<2> Tag content injected using the `nested-content` key in the tag template.

=== Engine Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

/**
* This node holds a single expression such as {@code foo.bar}.
*/
class ExpressionNode implements TemplateNode {
class ExpressionNode implements TemplateNode, Function<Object, CompletionStage<ResultNode>> {

final ExpressionImpl expression;
private final Engine engine;
Expand All @@ -22,8 +23,18 @@ public ExpressionNode(ExpressionImpl expression, Engine engine, Origin origin) {

@Override
public CompletionStage<ResultNode> resolve(ResolutionContext context) {
return context.evaluate(expression)
.thenCompose(r -> CompletableFuture.<ResultNode> completedFuture(new SingleResultNode(r, this)));
return context.evaluate(expression).thenCompose(this);
}

@Override
public CompletionStage<ResultNode> apply(Object result) {
if (result instanceof ResultNode) {
return CompletableFuture.completedFuture((ResultNode) result);
} else if (result instanceof CompletionStage) {
return ((CompletionStage<?>) result).thenCompose(this);
} else {
return CompletableFuture.completedFuture(new SingleResultNode(result, this));
}
}

public Origin getOrigin() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ static CompletionStage<Map<String, Object>> evaluateParams(Map<String, Expressio
result.completeExceptionally(t1);
} else {
// Build a map from the params
// IMPL NOTE: Keep the map mutable - it can be modified in UserTagSectionHelper
Map<String, Object> paramValues = new HashMap<>();
int j = 0;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public final class SectionBlock {
*/
List<TemplateNode> nodes;

public SectionBlock(Origin origin, String id, String label, Map<String, String> parameters,
SectionBlock(Origin origin, String id, String label, Map<String, String> parameters,
Map<String, Expression> expressions,
List<TemplateNode> nodes) {
this.origin = origin;
Expand All @@ -50,6 +50,10 @@ public SectionBlock(Origin origin, String id, String label, Map<String, String>
this.nodes = ImmutableList.copyOf(nodes);
}

public boolean isEmpty() {
return nodes.isEmpty();
}

Set<Expression> getExpressions() {
Set<Expression> expressions = new HashSet<>();
expressions.addAll(this.expressions.values());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,25 +98,25 @@ default boolean hasParameter(String name) {
*/
public interface SectionInitContext extends ParserDelegate {

default Map<String, String> getParameters() {
default public Map<String, String> getParameters() {
return getBlocks().get(0).parameters;
}

default boolean hasParameter(String name) {
default public boolean hasParameter(String name) {
return getParameters().containsKey(name);
}

default String getParameter(String name) {
default public String getParameter(String name) {
return getParameters().get(name);
}

Expression getExpression(String parameterName);
public Expression getExpression(String parameterName);

Expression parseValue(String value);
public Expression parseValue(String value);

List<SectionBlock> getBlocks();
public List<SectionBlock> getBlocks();

Engine getEngine();
public Engine getEngine();

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static io.quarkus.qute.Futures.evaluateParams;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand All @@ -12,37 +13,44 @@
public class UserTagSectionHelper implements SectionHelper {

private static final String IT = "it";
private static final String NESTED_CONTENT = "nested-content";

private final Supplier<Template> templateSupplier;
private final Map<String, Expression> parameters;
private final boolean isEmpty;

public UserTagSectionHelper(Supplier<Template> templateSupplier, Map<String, Expression> parameters) {
UserTagSectionHelper(Supplier<Template> templateSupplier, Map<String, Expression> parameters, boolean isEmpty) {
this.templateSupplier = templateSupplier;
this.parameters = parameters;
this.isEmpty = isEmpty;
}

@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
CompletableFuture<ResultNode> result = new CompletableFuture<>();
evaluateParams(parameters, context.resolutionContext()).whenComplete((r1, t1) -> {
evaluateParams(parameters, context.resolutionContext()).whenComplete((evaluatedParams, t1) -> {
if (t1 != null) {
result.completeExceptionally(t1);
} else {
// Execute the template with the params as the root context object
if (!isEmpty) {
// Execute the nested content first and make it accessible via the "nested-content" key
evaluatedParams.put(NESTED_CONTENT,
context.execute(context.resolutionContext().createChild(new HashMap<>(evaluatedParams), null)));
}
try {
// Execute the template with the params as the root context object
TemplateImpl tagTemplate = (TemplateImpl) templateSupplier.get();
tagTemplate.root.resolve(context.resolutionContext().createChild(r1, null))
.whenComplete((r2, t2) -> {
tagTemplate.root.resolve(context.resolutionContext().createChild(evaluatedParams, null))
.whenComplete((resultNode, t2) -> {
if (t2 != null) {
result.completeExceptionally(t2);
} else {
result.complete(r2);
result.complete(resultNode);
}
});
} catch (Throwable e) {
result.completeExceptionally(e);
}

}
});
return result;
Expand Down Expand Up @@ -78,6 +86,7 @@ public UserTagSectionHelper initialize(SectionInitContext context) {

Map<String, Expression> params = context.getParameters().entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> context.parseValue(e.getValue())));
boolean isEmpty = context.getBlocks().size() == 1 && context.getBlocks().get(0).isEmpty();

return new UserTagSectionHelper(new Supplier<Template>() {

Expand All @@ -89,7 +98,7 @@ public Template get() {
}
return template;
}
}, params);
}, params, isEmpty);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,49 @@ public void testUserTag() {
engine.parse("{#each this}{#myTag it showImage=true /}{/each}").render(Collections.singletonList(order)));
}

@Test
public void testUserTagWithNestedContent() {
Engine engine = Engine.builder().addDefaultSectionHelpers().addDefaultValueResolvers()
.addSectionHelper(new UserTagSectionHelper.Factory("myTag", "my-tag-id"))
.build();

Template tag = engine.parse("{#if showImage}<b>{nested-content}</b>{#else}nope{/if}");
engine.putTemplate("my-tag-id", tag);

Map<String, Object> order = new HashMap<>();
order.put("name", "Herbert");
assertEquals("<b>Herbert</b>",
engine.parse("{#myTag showImage=true}{order.name}{/}").render(Collections.singletonMap("order", order)));
assertEquals("nope", engine.parse("{#myTag}{order.name}{/}").render(Collections.singletonMap("order", order)));
assertEquals("nope",
engine.parse("{#myTag showImage=false}{order.name}{/}").render(Collections.singletonMap("order", order)));
assertEquals("nope",
engine.parse("{#each this}{#myTag showImage=false}{it.name}{/}{/each}")
.render(Collections.singletonMap("order", order)));
assertEquals("<b>Herbert</b>",
engine.parse("{#each this}{#myTag showImage=true}{it.name}{/}{/each}")
.render(Collections.singletonList(order)));
}

@Test
public void testUserTagWithRichNestedContent() {
Engine engine = Engine.builder().addDefaultSectionHelpers().addDefaultValueResolvers()
.addSectionHelper(new UserTagSectionHelper.Factory("myTag", "my-tag-id"))
.addSectionHelper(new UserTagSectionHelper.Factory("myTag2", "my-tag-id2"))
.build();

Template tag = engine.parse("{#if showImage}<b>{nested-content}</b>{#else}nope{/if}");
engine.putTemplate("my-tag-id", tag);

Template tag2 = engine.parse("{#if showImage2}<i>{nested-content}</i>{#else}nope2{/if}");
engine.putTemplate("my-tag-id2", tag2);

Map<String, Object> order = new HashMap<>();
order.put("name", "Herbert");
assertEquals("<b><i>Herbert</i></b>",
engine.parse("{#myTag showImage=true}{#myTag2 showImage2=true}{order.name}{/myTag2}{/myTag}")
.render(Collections.singletonMap("order", order)));

}

}