Skip to content

Commit 7655d65

Browse files
committed
Qute: add fragment named resolvers and capture alias for hidden fragment
- resolves #46355
1 parent 623bbf9 commit 7655d65

File tree

16 files changed

+424
-33
lines changed

16 files changed

+424
-33
lines changed

docs/src/main/asciidoc/qute-reference.adoc

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,8 +1258,8 @@ This is {#insert title}my title{/title}! <1>
12581258
[[fragments]]
12591259
==== Fragments
12601260

1261-
A fragment represents a part of the template that can be treated as a separate template, i.e. rendered separately.
1262-
One of the main motivations to introduce this feature was to support use cases like https://htmx.org/essays/template-fragments/[htmx fragments].
1261+
A fragment represents a part of a template that can be treated as a separate template, i.e. rendered separately.
1262+
One of the main motivations to introduce this feature was the support of use cases like https://htmx.org/essays/template-fragments/[htmx fragments].
12631263

12641264
Fragments can be defined with the `{#fragment}` section.
12651265
Each fragment has an identifier that can only consist of alphanumeric characters and underscores.
@@ -1285,7 +1285,7 @@ NOTE: Note that a fragment identifier must be unique in a template.
12851285
</ol>
12861286
{/fragment}
12871287
----
1288-
<1> Defines a fragment with identifier `item_aliases`. Note that only alphanumeric characters and underscores can be used in the identifier.
1288+
<1> Defines a fragment with identifier `item_aliases`. Note that only alphanumeric characters and underscores can be used in the identifier. The name of the first parameter can be omitted - `{#fragment item_aliases}` is fine too.
12891289

12901290
You can obtain a fragment programmatically via the `io.quarkus.qute.Template.getFragment(String)` method.
12911291

@@ -1318,49 +1318,63 @@ The snippet above should render something like:
13181318
TIP: In Quarkus, it is also possible to define a <<type_safe_fragments,type-safe fragment>>.
13191319

13201320
You can also include a fragment with an `{#include}` section inside another template or the template that defines the fragment.
1321+
A fragment can be also used in expressions with the `frg:`/`fragment:` namespaces.
13211322

13221323
.Including a Fragment in `user.html`
13231324
[source,html]
13241325
----
13251326
<h1>User - {user.name}</h1>
13261327
1328+
<p>
1329+
{#fragment fullname}
1330+
{name} <strong>{surname}</strong>
1331+
{/fragment}
1332+
</p>
1333+
13271334
<p>This document contains a detailed info about a user.</p>
13281335
13291336
{#include item$item_aliases aliases=user.aliases /} <1><2>
1337+
1338+
{frg:fullname} is a happy user! <3>
13301339
----
13311340
<1> A template identifier that contains a dollar sign `$` denotes a fragment. The `item$item_aliases` value is translated as: _Use the fragment `item_aliases` from the template `item`._
13321341
<2> The `aliases` parameter is used to pass the relevant data. We need to make sure that the data are set correctly. In this particular case the fragment will use the expression `user.aliases` as the value of `aliases` in the `{#for alias in aliases}` section.
1342+
<3> The `{frg:username}` expression outputs the fragment content. `frg:` can be replaced with `fragment:`.
13331343

13341344
TIP: If you want to reference a fragment from the same template, skip the part before `$`, i.e. something like `{#include $item_aliases /}`.
13351345

13361346
NOTE: You can specify `{#include item$item_aliases _ignoreFragments=true /}` in order to disable this feature, i.e. a dollar sign `$` in the template identifier does not result in a fragment lookup.
13371347

1338-
===== Hidden Fragments
1348+
1349+
===== Hidden Fragments (Capture)
13391350

13401351
By default, a fragment is normally rendered as a part of the original template.
1341-
However, sometimes it might be useful to mark a fragment as _hidden_ with `rendered=false`.
1342-
An interesting use case would be a fragment that can be used multiple-times inside the template that defines it.
1352+
However, sometimes it might be useful to mark a fragment as _hidden_.
1353+
The regular fragment section has the `capture` alias that implies a hidden fragment.
1354+
Alternatively, you can "hide" a fragment either with `rendered=false` or `_hidden` parameters.
1355+
An interesting use case could be a fragment that can be used multiple-times inside the template that defines it.
13431356

1344-
.Fragment Definition in `item.html`
1357+
.Hidden Fragment Definition in `item.html`
13451358
[source,html]
13461359
----
1347-
{#fragment id=strong rendered=false} <1>
1360+
{#capture strong} <1>
13481361
<strong>{val}</strong>
1349-
{/fragment}
1362+
{/capture}
13501363
13511364
<h1>My page</h1>
13521365
<p>This document
13531366
{#include $strong val='contains' /} <2>
13541367
a lot of
1355-
{#include $strong val='information' /} <3>
1368+
{capture:strong(param:val = 'information')} <3> <4>
13561369
!</p>
13571370
----
13581371
<1> Defines a hidden fragment with identifier `strong`.
1359-
In this particular case, we use the `false` boolean literal as the value of the `rendered` parameter.
1360-
However, it's possible to use any expression there.
1372+
`{#capture strong}` can be replaced with `{#fragment strong rendered=false}` or `{#fragment strong _hidden}`.
1373+
The `rendered` parameter can use any expression, e.g. `{#fragment strong rendered=config.isRendered}`.
13611374
<2> Include the fragment `strong` and pass the value.
13621375
Note the syntax `$strong` which is translated to include the fragment `strong` from the current template.
1363-
<3> Include the fragment `strong` and pass the value.
1376+
<3> A namespace resolver can be used to access a hidden fragment too. `capture:` can be replaced with `cap:`.
1377+
<4> `param:val = 'information'` is used to pass a named parameter to the fragment.
13641378

13651379
The snippet above renders something like:
13661380

@@ -1374,6 +1388,8 @@ a lot of
13741388
!</p>
13751389
----
13761390

1391+
TIP: In Quarkus, the namespace resolvers are automatically registered for namespaces `frg`, `fragment`, `cap` and `capture`.
1392+
13771393
==== Eval Section
13781394

13791395
This section can be used to parse and evaluate a template dynamically.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.quarkus.qute.deployment.fragment;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import jakarta.inject.Inject;
6+
7+
import org.jboss.shrinkwrap.api.asset.StringAsset;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.qute.Template;
12+
import io.quarkus.test.QuarkusUnitTest;
13+
14+
public class HiddenFragmentsTest {
15+
16+
@RegisterExtension
17+
static final QuarkusUnitTest config = new QuarkusUnitTest()
18+
.withApplicationRoot(root -> root
19+
.addAsResource(new StringAsset("""
20+
{#capture faClass}
21+
{#when type}
22+
{#is "info"}
23+
fa fa-lightbulb
24+
{#is "warning"}
25+
fa fa-exclamation-triangle
26+
{#is "error"}
27+
fa fa-times-circle
28+
{#is "success"}
29+
fa fa-check-circle
30+
{#is "question"}
31+
fa fa-question-circle
32+
{#else}
33+
fa fa-info-circle
34+
{/when}
35+
{/}
36+
<i class="{capture:faClass(param:type = type.or(anotherType)).strip}"></i>::{capture:faClass.strip}
37+
"""), "templates/hide.html"));
38+
39+
@Inject
40+
Template hide;
41+
42+
@Test
43+
public void testResolvers() {
44+
assertEquals("<i class=\"fa fa-times-circle\"></i>::fa fa-times-circle",
45+
hide.data("type", "error", "anotherType", "foo").render().strip());
46+
assertEquals("<i class=\"fa fa-question-circle\"></i>::fa fa-info-circle",
47+
hide.data("type", null, "anotherType", "question").render().strip());
48+
}
49+
50+
}

extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
import java.util.concurrent.ConcurrentMap;
2121
import java.util.regex.Pattern;
2222

23+
import jakarta.enterprise.context.ApplicationScoped;
24+
import jakarta.enterprise.context.Dependent;
25+
import jakarta.enterprise.event.Event;
26+
import jakarta.enterprise.event.Observes;
27+
import jakarta.enterprise.inject.Produces;
28+
import jakarta.inject.Singleton;
29+
import jakarta.interceptor.Interceptor;
30+
2331
import org.jboss.logging.Logger;
2432

2533
import io.quarkus.arc.All;
@@ -35,6 +43,7 @@
3543
import io.quarkus.qute.HtmlEscaper;
3644
import io.quarkus.qute.ImmutableList;
3745
import io.quarkus.qute.JsonEscaper;
46+
import io.quarkus.qute.NamedArgument;
3847
import io.quarkus.qute.NamespaceResolver;
3948
import io.quarkus.qute.ParserHook;
4049
import io.quarkus.qute.Qute;
@@ -58,13 +67,6 @@
5867
import io.quarkus.runtime.LocalesBuildTimeConfig;
5968
import io.quarkus.runtime.ShutdownEvent;
6069
import io.quarkus.runtime.Startup;
61-
import jakarta.enterprise.context.ApplicationScoped;
62-
import jakarta.enterprise.context.Dependent;
63-
import jakarta.enterprise.event.Event;
64-
import jakarta.enterprise.event.Observes;
65-
import jakarta.enterprise.inject.Produces;
66-
import jakarta.inject.Singleton;
67-
import jakarta.interceptor.Interceptor;
6870

6971
@Startup(Interceptor.Priority.PLATFORM_BEFORE)
7072
@Singleton
@@ -129,6 +131,8 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
129131
builder.addValueResolver(ValueResolvers.orEmpty());
130132
// Note that arrays are handled specifically during validation
131133
builder.addValueResolver(ValueResolvers.arrayResolver());
134+
// Named arguments for fragment namespace resolver
135+
builder.addValueResolver(new NamedArgument.SetValueResolver());
132136
// Additional value resolvers
133137
for (ValueResolver valueResolver : valueResolvers) {
134138
builder.addValueResolver(valueResolver);
@@ -197,6 +201,12 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
197201
}
198202
// str:eval
199203
builder.addNamespaceResolver(new StrEvalNamespaceResolver());
204+
// Fragment namespace resolvers
205+
builder.addNamespaceResolver(new NamedArgument.ParamNamespaceResolver());
206+
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.FRAGMENT));
207+
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.FRG));
208+
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.CAPTURE));
209+
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.CAP));
200210

201211
// Add generated resolvers
202212
for (String resolverClass : context.getResolverClasses()) {

extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.quarkus.qute.ParameterDeclaration;
3535
import io.quarkus.qute.RenderedResults;
3636
import io.quarkus.qute.ResultsCollectingTemplateInstance;
37+
import io.quarkus.qute.SectionNode;
3738
import io.quarkus.qute.Template;
3839
import io.quarkus.qute.TemplateInstance;
3940
import io.quarkus.qute.TemplateInstanceBase;
@@ -172,6 +173,14 @@ public Template get() {
172173
this.renderedResults = renderedResults;
173174
}
174175

176+
@Override
177+
public SectionNode getRootNode() {
178+
if (unambiguousTemplate != null) {
179+
return unambiguousTemplate.get().getRootNode();
180+
}
181+
throw ambiguousTemplates("getRootNode()");
182+
}
183+
175184
@Override
176185
public TemplateInstance instance() {
177186
TemplateInstance instance = new InjectableTemplateInstanceImpl();
@@ -322,6 +331,11 @@ public List<TemplateNode> getNodes() {
322331
return InjectableTemplate.this.getNodes();
323332
}
324333

334+
@Override
335+
public SectionNode getRootNode() {
336+
return InjectableTemplate.this.getRootNode();
337+
}
338+
325339
@Override
326340
public Collection<TemplateNode> findNodes(Predicate<TemplateNode> predicate) {
327341
return InjectableTemplate.this.findNodes(predicate);

independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalSectionHelper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
5555
}
5656

5757
private void parseAndResolve(CompletableFuture<ResultNode> ret, String contents, ResolutionContext resolutionContext) {
58-
TemplateImpl template;
58+
Template template;
5959
try {
60-
template = (TemplateImpl) engine.parse(contents);
61-
template.root
60+
template = engine.parse(contents);
61+
template.getRootNode()
6262
.resolve(resolutionContext)
6363
.whenComplete((resultNode, t2) -> {
6464
if (t2 != null) {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package io.quarkus.qute;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.CompletionStage;
7+
import java.util.concurrent.ExecutionException;
8+
9+
import io.quarkus.qute.EngineBuilder.EngineListener;
10+
import io.quarkus.qute.Template.Fragment;
11+
12+
/**
13+
* Renders a matching fragment from the current template.
14+
*
15+
* @see FragmentSectionHelper
16+
*/
17+
public class FragmentNamespaceResolver implements NamespaceResolver, EngineListener {
18+
19+
public static final String FRG = "frg";
20+
public static final String FRAGMENT = "fragment";
21+
public static final String CAP = "cap";
22+
public static final String CAPTURE = "capture";
23+
24+
private final String namespace;
25+
26+
private final int priority;
27+
28+
private volatile Engine engine;
29+
30+
public FragmentNamespaceResolver() {
31+
this(FRG, -1);
32+
}
33+
34+
public FragmentNamespaceResolver(String namespace) {
35+
this(namespace, -1);
36+
}
37+
38+
public FragmentNamespaceResolver(String namespace, int priority) {
39+
this.namespace = namespace;
40+
this.priority = priority;
41+
}
42+
43+
@Override
44+
public void engineBuilt(Engine engine) {
45+
this.engine = engine;
46+
}
47+
48+
@Override
49+
public CompletionStage<Object> resolve(EvalContext context) {
50+
String id = context.getName();
51+
Template template = null;
52+
int idx = id.lastIndexOf('$');
53+
if (idx != -1) {
54+
// the part before the last occurence of a dollar sign is the template identifier
55+
String templateId = id.substring(0, idx);
56+
Engine e = engine;
57+
if (e == null) {
58+
throw new TemplateException("Engine not set");
59+
}
60+
template = e.getTemplate(templateId);
61+
if (template == null) {
62+
throw new TemplateException("Template not found: " + templateId);
63+
}
64+
// the part after the last occurence of a dollar sign is the fragment identifier
65+
id = id.substring(idx + 1);
66+
} else {
67+
template = context.resolutionContext().getTemplate();
68+
}
69+
Fragment fragment = template.getFragment(id);
70+
if (fragment != null) {
71+
CompletableFuture<Object> ret = new CompletableFuture<>();
72+
if (!context.getParams().isEmpty()) {
73+
EvaluatedParams params = EvaluatedParams.evaluate(context);
74+
params.stage.whenComplete((r, t) -> {
75+
if (t != null) {
76+
ret.completeExceptionally(t);
77+
} else {
78+
Map<String, Object> args = new HashMap<>();
79+
for (int i = 0; i < context.getParams().size(); i++) {
80+
try {
81+
Object result = params.getResult(i);
82+
if (result instanceof NamedArgument arg) {
83+
args.put(arg.getName(), arg.getValue());
84+
} else {
85+
ret.completeExceptionally(
86+
new TemplateException("Named argument expected: " + result.getClass()));
87+
break;
88+
}
89+
} catch (InterruptedException | ExecutionException e) {
90+
ret.completeExceptionally(e);
91+
}
92+
}
93+
ResolutionContext child = context.resolutionContext().createChild(Mapper.wrap(args), null);
94+
fragment.getRootNode().resolve(child, Map.of(Template.Fragment.ATTRIBUTE, true))
95+
.whenComplete((r2, t2) -> {
96+
if (t2 != null) {
97+
ret.completeExceptionally(t2);
98+
} else {
99+
StringBuilder sb = new StringBuilder();
100+
r2.process(sb::append);
101+
ret.complete(sb.toString());
102+
}
103+
});
104+
}
105+
});
106+
} else {
107+
fragment.getRootNode().resolve(context.resolutionContext(), Map.of(Template.Fragment.ATTRIBUTE, true))
108+
.whenComplete((r, t) -> {
109+
if (t != null) {
110+
ret.completeExceptionally(t);
111+
} else {
112+
StringBuilder sb = new StringBuilder();
113+
r.process(sb::append);
114+
ret.complete(sb.toString());
115+
}
116+
});
117+
}
118+
return ret;
119+
}
120+
return Results.notFound(context);
121+
}
122+
123+
@Override
124+
public int getPriority() {
125+
return priority;
126+
}
127+
128+
@Override
129+
public String getNamespace() {
130+
return namespace;
131+
}
132+
133+
}

0 commit comments

Comments
 (0)