Skip to content
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
42 changes: 29 additions & 13 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1258,8 +1258,8 @@ This is {#insert title}my title{/title}! <1>
[[fragments]]
==== Fragments

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

Fragments can be defined with the `{#fragment}` section.
Each fragment has an identifier that can only consist of alphanumeric characters and underscores.
Expand All @@ -1285,7 +1285,7 @@ NOTE: Note that a fragment identifier must be unique in a template.
</ol>
{/fragment}
----
<1> Defines a fragment with identifier `item_aliases`. Note that only alphanumeric characters and underscores can be used in the identifier.
<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.

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

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

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

.Including a Fragment in `user.html`
[source,html]
----
<h1>User - {user.name}</h1>

<p>
{#fragment fullname}
{name} <strong>{surname}</strong>
{/fragment}
</p>

<p>This document contains a detailed info about a user.</p>

{#include item$item_aliases aliases=user.aliases /} <1><2>

{frg:fullname} is a happy user! <3>
----
<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`._
<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.
<3> The `{frg:username}` expression outputs the fragment content. `frg:` can be replaced with `fragment:`.

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

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.

===== Hidden Fragments

===== Hidden Fragments (Capture)

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

.Fragment Definition in `item.html`
.Hidden Fragment Definition in `item.html`
[source,html]
----
{#fragment id=strong rendered=false} <1>
{#capture strong} <1>
<strong>{val}</strong>
{/fragment}
{/capture}

<h1>My page</h1>
<p>This document
{#include $strong val='contains' /} <2>
a lot of
{#include $strong val='information' /} <3>
{capture:strong(param:val = 'information')} <3> <4>
!</p>
----
<1> Defines a hidden fragment with identifier `strong`.
In this particular case, we use the `false` boolean literal as the value of the `rendered` parameter.
However, it's possible to use any expression there.
`{#capture strong}` can be replaced with `{#fragment strong rendered=false}` or `{#fragment strong _hidden}`.
The `rendered` parameter can use any expression, e.g. `{#fragment strong rendered=config.isRendered}`.
<2> Include the fragment `strong` and pass the value.
Note the syntax `$strong` which is translated to include the fragment `strong` from the current template.
<3> Include the fragment `strong` and pass the value.
<3> A namespace resolver can be used to access a hidden fragment too. `capture:` can be replaced with `cap:`.
<4> `param:val = 'information'` is used to pass a named parameter to the fragment.

The snippet above renders something like:

Expand All @@ -1374,6 +1388,8 @@ a lot of
!</p>
----

TIP: In Quarkus, the namespace resolvers are automatically registered for namespaces `frg`, `fragment`, `cap` and `capture`.

==== Eval Section

This section can be used to parse and evaluate a template dynamically.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.quarkus.qute.deployment.fragment;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Template;
import io.quarkus.test.QuarkusUnitTest;

public class HiddenFragmentsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addAsResource(new StringAsset("""
{#capture faClass}
{#when type}
{#is "info"}
fa fa-lightbulb
{#is "warning"}
fa fa-exclamation-triangle
{#is "error"}
fa fa-times-circle
{#is "success"}
fa fa-check-circle
{#is "question"}
fa fa-question-circle
{#else}
fa fa-info-circle
{/when}
{/}
<i class="{capture:faClass(param:type = type.or(anotherType)).strip}"></i>::{capture:faClass.strip}
"""), "templates/hide.html"));

@Inject
Template hide;

@Test
public void testResolvers() {
assertEquals("<i class=\"fa fa-times-circle\"></i>::fa fa-times-circle",
hide.data("type", "error", "anotherType", "foo").render().strip());
assertEquals("<i class=\"fa fa-question-circle\"></i>::fa fa-info-circle",
hide.data("type", null, "anotherType", "question").render().strip());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@
import io.quarkus.qute.EngineBuilder;
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.Expression;
import io.quarkus.qute.FragmentNamespaceResolver;
import io.quarkus.qute.HtmlEscaper;
import io.quarkus.qute.ImmutableList;
import io.quarkus.qute.JsonEscaper;
import io.quarkus.qute.NamedArgument;
import io.quarkus.qute.NamespaceResolver;
import io.quarkus.qute.ParserHook;
import io.quarkus.qute.Qute;
Expand Down Expand Up @@ -129,6 +131,8 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
builder.addValueResolver(ValueResolvers.orEmpty());
// Note that arrays are handled specifically during validation
builder.addValueResolver(ValueResolvers.arrayResolver());
// Named arguments for fragment namespace resolver
builder.addValueResolver(new NamedArgument.SetValueResolver());
// Additional value resolvers
for (ValueResolver valueResolver : valueResolvers) {
builder.addValueResolver(valueResolver);
Expand Down Expand Up @@ -196,8 +200,13 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
builder.addNamespaceResolver(namespaceResolver);
}
// str:eval
StrEvalNamespaceResolver strEvalNamespaceResolver = new StrEvalNamespaceResolver();
builder.addNamespaceResolver(strEvalNamespaceResolver);
builder.addNamespaceResolver(new StrEvalNamespaceResolver());
// Fragment namespace resolvers
builder.addNamespaceResolver(new NamedArgument.ParamNamespaceResolver());
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.FRAGMENT));
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.FRG));
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.CAPTURE));
builder.addNamespaceResolver(new FragmentNamespaceResolver(FragmentNamespaceResolver.CAP));

// Add generated resolvers
for (String resolverClass : context.getResolverClasses()) {
Expand Down Expand Up @@ -273,9 +282,6 @@ public void run() {

engine = builder.build();

// Init resolver for str:eval
strEvalNamespaceResolver.setEngine(engine);

// Load discovered template files
Map<String, List<Template>> discovered = new HashMap<>();
for (String path : context.getTemplatePaths()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.quarkus.qute.ParameterDeclaration;
import io.quarkus.qute.RenderedResults;
import io.quarkus.qute.ResultsCollectingTemplateInstance;
import io.quarkus.qute.SectionNode;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.TemplateInstanceBase;
Expand Down Expand Up @@ -172,6 +173,14 @@ public Template get() {
this.renderedResults = renderedResults;
}

@Override
public SectionNode getRootNode() {
if (unambiguousTemplate != null) {
return unambiguousTemplate.get().getRootNode();
}
throw ambiguousTemplates("getRootNode()");
}

@Override
public TemplateInstance instance() {
TemplateInstance instance = new InjectableTemplateInstanceImpl();
Expand Down Expand Up @@ -322,6 +331,11 @@ public List<TemplateNode> getNodes() {
return InjectableTemplate.this.getNodes();
}

@Override
public SectionNode getRootNode() {
return InjectableTemplate.this.getRootNode();
}

@Override
public Collection<TemplateNode> findNodes(Predicate<TemplateNode> predicate) {
return InjectableTemplate.this.findNodes(predicate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jboss.logging.Logger;

/**
* Builder for {@link Engine}.
* <p>
Expand All @@ -24,6 +27,8 @@
*/
public final class EngineBuilder {

private static final Logger LOG = Logger.getLogger(EngineBuilder.class);

final Map<String, SectionHelperFactory<?>> sectionHelperFactories;
final List<ValueResolver> valueResolvers;
final List<NamespaceResolver> namespaceResolvers;
Expand All @@ -37,6 +42,7 @@ public final class EngineBuilder {
String iterationMetadataPrefix;
long timeout;
boolean useAsyncTimeout;
final List<EngineListener> listeners;

EngineBuilder() {
this.sectionHelperFactories = new HashMap<>();
Expand All @@ -51,6 +57,7 @@ public final class EngineBuilder {
this.iterationMetadataPrefix = LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE;
this.timeout = 10_000;
this.useAsyncTimeout = true;
this.listeners = new ArrayList<>();
}

/**
Expand Down Expand Up @@ -103,20 +110,38 @@ public EngineBuilder addDefaultSectionHelpers() {
new FragmentSectionHelper.Factory());
}

/**
*
* @param resolverSupplier
* @return self
* @see EngineListener
*/
public EngineBuilder addValueResolver(Supplier<ValueResolver> resolverSupplier) {
return addValueResolver(resolverSupplier.get());
}

/**
*
* @param resolvers
* @return self
* @see EngineListener
*/
public EngineBuilder addValueResolvers(ValueResolver... resolvers) {
for (ValueResolver valueResolver : resolvers) {
addValueResolver(valueResolver);
}
return this;
}

/**
*
* @param resolver
* @return self
* @see EngineListener
*/
public EngineBuilder addValueResolver(ValueResolver resolver) {
this.valueResolvers.add(resolver);
return this;
return addListener(resolver);
}

/**
Expand Down Expand Up @@ -150,6 +175,7 @@ public EngineBuilder addDefaults() {
* @param resolver
* @return self
* @throws IllegalArgumentException if there is a resolver of the same priority for the given namespace
* @see EngineListener
*/
public EngineBuilder addNamespaceResolver(NamespaceResolver resolver) {
String namespace = Namespaces.requireValid(resolver.getNamespace());
Expand All @@ -163,7 +189,7 @@ public EngineBuilder addNamespaceResolver(NamespaceResolver resolver) {
}
}
this.namespaceResolvers.add(resolver);
return this;
return addListener(resolver);
}

/**
Expand Down Expand Up @@ -299,12 +325,31 @@ public EngineBuilder useAsyncTimeout(boolean value) {
return this;
}

/**
* Value and namespace resolvers that also implement {@link EngineListener} are registered automatically.
*
* @param listener
* @return self
*/
public EngineBuilder addEngineListener(EngineListener listener) {
this.listeners.add(Objects.requireNonNull(listener));
return this;
}

/**
*
* @return a new engine instance
*/
public Engine build() {
return new EngineImpl(this);
EngineImpl engine = new EngineImpl(this);
for (EngineListener listener : listeners) {
try {
listener.engineBuilt(engine);
} catch (Throwable e) {
LOG.warnf("Engine listener error: " + e);
}
}
return engine;
}

private SectionHelperFactory<?> cachedFactory(SectionHelperFactory<?> factory) {
Expand All @@ -314,6 +359,25 @@ private SectionHelperFactory<?> cachedFactory(SectionHelperFactory<?> factory) {
return new CachedConfigSectionHelperFactory<>(factory);
}

private EngineBuilder addListener(Object obj) {
if (obj instanceof EngineListener listener) {
listeners.add(listener);
}
return this;
}

/**
* Receives notifications about Engine lifecycle.
* <p>
* Value and namespace resolvers that also implement {@link EngineListener} are registered automatically.
*/
public interface EngineListener {

default void engineBuilt(Engine engine) {
}

}

static class CachedConfigSectionHelperFactory<T extends SectionHelper> implements SectionHelperFactory<T> {

private final SectionHelperFactory<T> delegate;
Expand Down
Loading
Loading