Skip to content

Commit

Permalink
feat: Add ability to render thymeleaf fragments (#678)
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesg committed Feb 14, 2024
1 parent 6035ef8 commit f0a5a4a
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@
import io.micronaut.views.exceptions.ViewRenderingException;
import jakarta.inject.Singleton;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.ExpressionContext;
import org.thymeleaf.context.IContext;
import org.thymeleaf.exceptions.TemplateEngineException;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.standard.expression.FragmentExpression;
import org.thymeleaf.standard.expression.StandardExpressions;
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

import java.io.Writer;
import java.util.Locale;
import java.util.Set;

/**
* Renders templates Thymeleaf Java template engine.
Expand Down Expand Up @@ -81,18 +86,39 @@ public Writable render(@NonNull String viewName,
ArgumentUtils.requireNonNull("viewName", viewName);
return (writer) -> {
IContext context = new WebContext(request, request != null ? httpLocaleResolver.resolveOrDefault(request) : Locale.getDefault(),
ViewUtils.modelOf(data));
render(viewName, context, writer);
ViewUtils.modelOf(data));

var templateAndFragment = resolveTemplate(viewName);

render(templateAndFragment.templateName, templateAndFragment.fragmentSelectors, context, writer);
};
}

/**
* Passes the arguments as is to {@link TemplateEngine#process(String, IContext, Writer)}.
*
* @param viewName The view name
* @param fragmentSelectors Fragment selectors
* @param context The context
* @param writer The writer
*/
public void render(String viewName, Set<String> fragmentSelectors, IContext context, Writer writer) {
try {
engine.process(viewName, fragmentSelectors, context, writer);
} catch (TemplateEngineException e) {
throw new ViewRenderingException("Error rendering Thymeleaf view [" + viewName + "]: " + e.getMessage(), e);
}
}

/**
* Passes the arguments as is to {@link TemplateEngine#process(String, IContext, Writer)}.
*
* @param viewName The view name
* @param context The context
* @param writer The writer
* @deprecated Use {@link #render(String, Set, IContext, Writer)} instead.
*/
@Deprecated(forRemoval = true, since = "5.2.0")
public void render(String viewName, IContext context, Writer writer) {
try {
engine.process(viewName, context, writer);
Expand All @@ -103,7 +129,8 @@ public void render(String viewName, IContext context, Writer writer) {

@Override
public boolean exists(@NonNull String viewName) {
String location = viewLocation(viewName);
var templateAndFragment = resolveTemplate(viewName);
String location = viewLocation(templateAndFragment.templateName);
return resourceLoader.getResourceAsStream(location).isPresent();
}

Expand Down Expand Up @@ -131,8 +158,30 @@ private ClassLoaderTemplateResolver initializeTemplateResolver(ViewsConfiguratio

private String viewLocation(final String name) {
return templateResolver.getPrefix() +
ViewUtils.normalizeFile(name, templateResolver.getSuffix()) +
templateResolver.getSuffix();
ViewUtils.normalizeFile(name, templateResolver.getSuffix()) +
templateResolver.getSuffix();
}

private record TemplateAndFragment(String templateName, Set<String> fragmentSelectors) {
}

private TemplateAndFragment resolveTemplate(String viewName) {
if (!viewName.contains("::")) {
return new TemplateAndFragment(viewName, null);
}

var expressionContext = new ExpressionContext(engine.getConfiguration());
var parser = StandardExpressions.getExpressionParser(engine.getConfiguration());
FragmentExpression fragmentExpression;
try {
fragmentExpression = (FragmentExpression) parser.parseExpression(expressionContext, "~{" + viewName + "}");
} catch (TemplateProcessingException e) {
throw new IllegalArgumentException("Invalid template name specification: '" + viewName + "'");
}
var fragment = FragmentExpression.createExecutedFragmentExpression(expressionContext, fragmentExpression);
var templateName = FragmentExpression.resolveTemplateName(fragment);
var fragmentSelectors = FragmentExpression.resolveFragments(fragment);

return new TemplateAndFragment(templateName, fragmentSelectors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.micronaut.views.thymeleaf

import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

import static io.micronaut.views.thymeleaf.WriteableUtils.*

@MicronautTest(startApplication = false)
class ThymeleafViewRenderFragmentSpec extends Specification {

@Inject
ThymeleafViewsRenderer<?> viewRenderer

void "can render fragment"() {
expect:
"<div>FRAGMENT</div>" == writableToString(viewRenderer.render("fragment :: thefragment", ["some": "data"], null))

and:
"<div>FRAGMENT 2</div>" == writableToString(viewRenderer.render("fragment :: thefragment2", ["some": "data"], null))
}

void "can render main body"() {
when:
String result = writableToString(viewRenderer.render("fragment", ["some": "data"], null))

then:
result.contains("MAIN") && result.contains("FRAGMENT")
}

void "exists is successful when using fragments"() {
expect:
viewRenderer.exists("fragment :: thefragment")
}

void "exists is successful when using regular view name"() {
expect:
viewRenderer.exists("fragment")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ class ThymeleafViewRenderNullableRequestSpec extends Specification {

void "views can be render with no request"() {
when:
Writable writeable = viewRenderer.render("tim", ["username": "Tim"], null)
String result = new StringWriter().with {
writeable.writeTo(it)
it.toString()
}
String result = WriteableUtils.writableToString(viewRenderer.render("tim", ["username": "Tim"], null))

then:
result.contains("username: <span>Tim</span>")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.micronaut.views.thymeleaf

import io.micronaut.core.io.Writable

final class WriteableUtils {
private WriteableUtils() {

}

static String writableToString(Writable writable) {
return new StringWriter().with {
writable.writeTo(it)
it.toString()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import io.micronaut.views.View
class LinkTestController {
@Get
@View("contextRelativeUrl")
public HttpResponse contextRelativeUrl() {
HttpResponse contextRelativeUrl() {
return HttpResponse.ok()
}

Expand Down
11 changes: 11 additions & 0 deletions views-thymeleaf/src/test/resources/views/fragment.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
</head>
<body>
<div>MAIN</div>
<div th:fragment="thefragment">FRAGMENT</div>
<div th:fragment="thefragment2">FRAGMENT 2</div>
</body>
</html>

0 comments on commit f0a5a4a

Please sign in to comment.