@Controller("/views")
class ViewsController {
@View("home")
@Get("/")
public HttpResponse<?> index() {
return HttpResponse.ok(CollectionUtils.mapOf("loggedIn", true, "username", "sdelamo"))
}
}
Table of Contents
Micronaut Views
Provides integration between Micronaut and server-side views technologies
Version: 5.2.0
1 Introduction
This project integrates the Micronaut framework and Server Side View Rendering.
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 Server Side View Rendering
Although the Micronaut framework is primarily designed around message encoding / decoding there are occasions where it is convenient to render a view on the server side.
The views
module provides support for view rendering on the server side and does so by rendering views on the I/O thread pool in order to avoid blocking the Netty event loop.
To use the view rendering features described in this section, add a dependency based on the view rendering engine you prefer (see the following sections).
For file-based view schemes, views and templates can be placed in the src/main/resources/views
directory of your
project. If you use this feature and wish to use a different folder, set the property micronaut.views.folder
.
Your controller’s method can render the response with a template by using the View annotation.
The following is an example of a controller which renders a template by passing a model as a java.util.Map
via the returned response object.
1 | Use @View annotation to indicate the view name which should be used to render a view for the route. |
In addition, you can return any POJO object and the properties of the POJO will be exposed to the view for rendering:
@Controller("/views")
class ViewsController {
@View("home")
@Get("/pojo")
public HttpResponse<Person> pojo() {
return HttpResponse.ok(new Person("sdelamo", true))
}
}
1 | Use @View annotation to indicate the view name which should be used to render the POJO responded by the controller. |
Use the @Introspected annotation on your POJO object to generate BeanIntrospection
metadata at compilation time.
You can also return a ModelAndView and skip specifying the View annotation.
@Controller("/views")
class ViewsController {
@Get("/modelAndView")
ModelAndView modelAndView() {
return new ModelAndView("home",
new Person("sdelamo", true))
}
Moreover, the Controller’s method can return a POJO annotated with @Introspected
to the view:
@View("fruits")
@Get
public Fruit index() {
return new Fruit("apple", "red");
}
@View("fruits")
@Get
Fruit index() {
return new Fruit("apple", "red")
}
@View("fruits")
@Get
fun index() = Fruit("apple", "red")
The following sections show different template engines integrations.
To create your own implementation create a class which implements ViewsRenderer and annotate it with @Produces to the media types the view rendering supports producing.
If you want to render a template directly on your code (for example, to generate the body of an email) you can inject the ViewsRenderer bean and use its method "render". |
3.1 Template Engines
Micronaut Views provides out-of-the-box support for multiple template languages.
3.1.1 Thymeleaf
Micronaut Views Thymeleaf includes ThymeleafViewsRenderer which uses the Thymeleaf Java template engine.
Add the micronaut-views-thymeleaf
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-thymeleaf")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-thymeleaf</artifactId>
</dependency>
Thymeleaf integration instantiates a ClassLoaderTemplateResolver
.
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The character encoding to use. Default value ("UTF-8"). |
|
org.thymeleaf.templatemode.TemplateMode |
The template mode to be used. |
|
java.lang.String |
The suffix to use. Default value (".html"). |
|
boolean |
Sets whether to force the suffix. Default value (false). |
|
boolean |
Whether to force template mode. Default value (false). |
|
long |
The cache TTL in millis. |
|
boolean |
Whether templates should be checked for existence. |
|
boolean |
Whether templates are cacheable. |
|
boolean |
Whether thymeleaf rendering is enabled. Default value (true). |
|
java.time.Duration |
Sets the cache TTL as a duration. |
The example shown in the Views section, could be rendered with the following Thymeleaf template:
<!DOCTYPE html>
<html lang="en" th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
</head>
<body>
<section>
<h1 th:if="${loggedIn}">username: <span th:text="${username}"></span></h1>
<h1 th:unless="${loggedIn}">You are not logged in</h1>
</section>
</body>
</html>
and layout:
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">Layout Title</title>
</head>
<body>
<h1>Layout H1</h1>
<div th:replace="${content}">
<p>Layout content</p>
</div>
<footer>
Layout footer
</footer>
</body>
</html>
Use Thymeleaf templates with Micronaut i18n messages. Internationalization messages will be resolved from Micronaut’s MessageSource automatically. For more information on the MessageSource, see the core documentation.
See the guide for Server-side HTML with Thymeleaf to learn more. |
3.1.2 Handlebars.java
Micronaut Views Handlebars includes HandlebarsViewsRenderer which uses the Handlebars.java project.
Add the following dependency on your classpath. For example, in build.gradle
implementation("io.micronaut.views:micronaut-views-handlebars")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-handlebars</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Handlebars template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
{{#if loggedIn}}
<h1>username: <span>{{username}}</span></h1>
{{else}}
<h1>You are not logged in</h1>
{{/if}}
</body>
</html>
3.1.3 Apache Velocity
Micronaut Views Velocity includes VelocityViewsRenderer which uses the Apache Velocity Java-based template engine.
Add the micronaut-views-velocity
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-velocity")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-velocity</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Velocity template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
#if( $loggedIn )
<h1>username: <span>$username</span></h1>
#else
<h1>You are not logged in</h1>
#end
</body>
</html>
3.1.4 Apache Freemarker
Micronaut Views Freemarker includes FreemarkerViewsRenderer which uses the Apache Freemarker Java-based template engine.
Add the following dependency on your classpath.
implementation("io.micronaut.views:micronaut-views-freemarker")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-freemarker</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Freemarker template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<#if loggedIn??>
<h1>username: <span>${username}</span></h1>
<#else>
<h1>You are not logged in</h1>
</#if>
</body>
</html>
Freemarker integration instantiates a freemarker Configuration
.
All configurable properties are extracted from Configuration
and
Configurable
, and properties names are reused in the Micronaut configuration.
If a value is not declared and is null, the default configuration from Freemarker is used. The expected format of each value is the same from Freemarker, and no conversion or validation is done by the Micronaut framework. You can find in Freemarker documentation how to configure each one.
3.1.5 Rocker
Micronaut Views Rocker includes RockerEngine which uses the Rocker Java-based template engine.
Add the micronaut-views-rocker
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-rocker")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-rocker</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Rocker template:
@args (Boolean loggedIn, String username)
<html>
<head>
<title>Home</title>
</head>
<body>
@if (loggedIn != null && loggedIn) {
<h1>username: <span>@username</span></h1>
} else {
<h1>You are not logged in</h1>
}
</body>
</html>
Compiling Templates
Rocker templates must be precompiled at build time. This can be done either by a Gradle or Maven plugin.
plugins {
id "com.fizzed.rocker" version "1.2.3"
}
sourceSets {
main {
rocker {
srcDir('src/main/resources')
}
}
}
<build>
<plugins>
<plugin>
<groupId>com.fizzed</groupId>
<artifactId>rocker-maven-plugin</artifactId>
<version>1.2.3</version>
<executions>
<execution>
<id>generate-rocker-templates</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<templateDirectory>src/main/resources</templateDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Compile Time Type Checking
Rocker templates are compiled at build time giving the ability to statically type check template arguments.
Using the standard @View
annotation will invoke the template dynamically without these checks.
Instead the compiled template should be invoked within the controller and a RockerWritable
returned.
import io.micronaut.views.rocker.RockerWritable;
import views.home;
...
@Get("/")
@Produces(TEXT_HTML)
public HttpResponse<?> staticTemplate() {
return HttpResponse.ok(new RockerWritable(home.template(true, "sdelamo")));
}
...
Configuration
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
boolean |
Whether Rocker views are enabled. Default value (true). |
|
java.lang.String |
The default extension to use for Rocker templates. Default value ("rocker.html"). |
|
boolean |
Whether hot reloading is enabled. Default value (false). |
|
boolean |
Whether relaxed binding is enabled for dynamic templates. Default value (false). |
3.1.6 Soy/Closure Support
Micronaut Views Soy includes SoySauceViewsRenderer which uses Soy, also known as Closure Templates, a template compiler from Google. Soy is usable standalone, or together with the rest of the Closure Tools ecosystem, and targets both server-side and client-side, with the ability to compile templates into Java, Python, or JavaScript.
Add the micronaut-views-soy
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-soy")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-soy</artifactId>
</dependency>
3.1.6.1 Soy Configuration
The entire set of supported Soy configuration properties are documented below:
Property | Type | Description |
---|---|---|
|
boolean |
Whether Soy-backed views are enabled. Default value (true). |
|
boolean |
Specifies whether renaming is enabled. Defaults value (true). |
3.1.6.2 Soy Usage
To use Soy integration you have to provide a bean of type SoyFileSetProvider. Such a bean is responsible for loading the Soy templates, either in compiled or source form.
Given the following Soy template:
{namespace sample}
// Sample template.
{template home}
{@param loggedIn: bool} // Whether the user is logged in.
{@param? username: string} // The user's username, if they are logged in.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<script type="text/javascript">alert("Hello, CSP!");</script>
</head>
<body>
{if $loggedIn}
<h1>username: <span>{$username}</span></h1>
{else}
<h1>You are not logged in</h1>
{/if}
</body>
</html>
{/template}
{template tim}
{@param? username: string}
username: <span>{$username}</span>
{/template}
A naive implementation of SoyFileSetProvider could look like:
@Singleton
public class CustomSoyFileSetProvider implements SoyFileSetProvider {
private static final Logger LOG = LoggerFactory.getLogger(CustomSoyFileSetProvider.class);
private static final String[] VIEWS = {
"home.soy"
};
private final String folder;
private final ResourceLoader resourceLoader;
public CustomSoyFileSetProvider(ViewsConfiguration viewsConfiguration, ResourceLoader resourceLoader) {
this.folder = viewsConfiguration.getFolder();
this.resourceLoader = resourceLoader;
}
@Override
public SoyFileSet provideSoyFileSet() {
final SoyFileSet.Builder builder = SoyFileSet.builder();
for (final String template : VIEWS) {
resourceLoader.getResource(folder + "/" + template).ifPresent(url -> {
try {
builder.add(new File(url.toURI()));
} catch (URISyntaxException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("URISyntaxException raised while generating the SoyFileSet for folder {}",
folder, e);
}
}
});
}
return builder.build();
}
}
@Singleton
class CustomSoyFileSetProvider implements SoyFileSetProvider {
private static final Logger LOG = LoggerFactory.getLogger(CustomSoyFileSetProvider.class)
private static final List<String> VIEWS = [
"home.soy"
]
private final String folder
private final ResourceLoader resourceLoader
CustomSoyFileSetProvider(ViewsConfiguration viewsConfiguration, ResourceLoader resourceLoader) {
this.folder = viewsConfiguration.getFolder()
this.resourceLoader = resourceLoader
}
@Override
SoyFileSet provideSoyFileSet() {
final SoyFileSet.Builder builder = SoyFileSet.builder()
for (String template : VIEWS) {
String path = "${folder}${template}"
resourceLoader.getResource(path).ifPresent(url -> {
try {
builder.add(new File(url.toURI()))
} catch (URISyntaxException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("URISyntaxException raised while generating SoyFileSet for folder {}", folder, e)
}
}
})
}
builder.build()
}
}
@Singleton
class CustomSoyFileSetProvider(
private val resourceLoader: ResourceLoader,
viewsConfiguration: ViewsConfiguration
) : SoyFileSetProvider {
private val folder: String = viewsConfiguration.folder
override fun provideSoyFileSet(): SoyFileSet {
val builder = SoyFileSet.builder()
for (template in VIEWS) {
resourceLoader.getResource("$folder/$template").ifPresent { url: URL ->
try {
builder.add(File(url.toURI()))
} catch (e: URISyntaxException) {
if (LOG.isWarnEnabled) {
LOG.warn("URISyntaxException raised while generating the SoyFileSet for folder {}", folder, e)
}
}
}
}
return builder.build()
}
companion object {
private val LOG = LoggerFactory.getLogger(CustomSoyFileSetProvider::class.java)
private val VIEWS = arrayOf("home.soy")
}
}
Soy allows users to write custom functions that templates can call, as described by their docs for Creating a Plugin. If you have one or more implementations of SoySourceFunction, you can register them in the SoyFileSetProvider.provideSoyFileSet() method using SoyFileSet builder’s addSourceFunction(SoySourceFunction) or addSourceFunction(Iterable<? extends SoySourceFunction>) methods.
|
The return value of the following Controller is converted to Soy template context parameters, and passed to the @View
-annotation-bound template.
@Controller("/soy")
public class SoyController {
@View("sample.home")
@Get
public HttpResponse<?> home() {
return HttpResponse.ok(CollectionUtils.mapOf("loggedIn", true, "username", "sgammon"));
}
}
@Controller("/soy")
class SoyController {
@View("sample.home")
@Get
HttpResponse<?> home() {
HttpResponse.ok([loggedIn: true, username: "sgammon"])
}
}
@Controller("/soy")
class SoyController {
@View("sample.home")
@Get
fun home() = HttpResponse.ok(mutableMapOf(
"loggedIn" to true,
"username" to "sgammon"
))
}
The View annotation value is set to namespace and template name. |
Both server-side Soy rendering layers are supported in Micronaut Views Soy:
-
SoySauceViewsRenderer
: This renderer uses templates pre-compiled to Java bytecode, generally AOT, withSoyToJbcSrcCompiler
. If compiled templates can’t be located by theSoyFileSetProvider
, templates are pre-compiled into bytecode at server startup. This can be impactful on startup-time, so, if that’s an important metric for your app, pre-compile your templates using the AOT bytecode compiler.
3.1.6.3 Soy Features - Template Security
Soy/Closure Templates has seen significant work from Google where security is concerned, both in the browser and on the
backend. Soy is extremely strict and strongly typed, with validation being performed by Soy and then subsequently either
by javac
or Closure Compiler, and additionally in some cases at render-time. What follows is a brief guide of these
security features. These are outlined in more detail over in the
Closure Templates docs.
Front-end Security
-
Trusted URIs: Soy is smart enough to know that
scriptUrl
might be influenced by user input, and, therefore, introduces an XSS vulnerability when used as a scriptsrc
. To inject resources, useuri
:/* I won't compile because I introduce a vulnerability */ {template .foo} {@param scriptUrl: string} <script src={$scriptUrl}></script> {/template}
/* I will compile because I accept a URI type, which is trusted in this context */ {template .foo} {@param scriptUrl: uri} <script src={$scriptUrl}></script> {/template}
-
Markup: Passing in HTML for
content
won’t work if it’s astring
. Soy will comply and inject your content, but it will be escaped. To inject markup, use the strict markup types (js
,css
,html
)./* I will compile, but I may escape the injected content if it contains markup. */ {template .foo} {@param content: string} <b>{$content}</b> {/template}
/* I allow markup because the appropriate types are used */ {template .foo} {@param content: html} <b>{$content}</b> {/template}
Back-end Security
-
CSP Nonce Support: Soy has support for Content Security Policy (Level 3), specifically, embedding server-generated
nonce
attributes in<script>
tags. This is accomplished by providing an "injected value" with the key"csp_nonce"
. The nonce, which should change on each page load, is available in the template like so:/* Soy will inject the nonce for you, but if you need it anyway, this is how you access it. */ {template .foo} {@param injectedScript: uri} {@inject csp_nonce: string} <script src={$injectedScript} nonce={$csp_nonce} type="text/javascript"></script> {/template}
3.1.6.4 Soy Features - Renaming
Soy has powerful built-in renaming features, that both obfuscate and optimize your code as it is rendered. Renaming is opt-in and requires a few things:
-
A compatible CSS compiler (GSS / Closure Stylesheets is a good one)
-
Special calls in your templates that map CSS classes and IDs
-
JSON renaming map provided at compile time for JS templates, and runtime for Java rendering
Renaming is similar to other frameworks' corresponding uglify features, but it’s significantly more powerful, allowing you to rewrite CSS classes and IDs as you would JavaScript symbols. Here’s how you can use renaming server-side with Micronaut Views Soy:
-
Configure Micronaut Views Soy to enable renaming:
micronaut.views.soy.enabled=true
micronaut.views.soy.renaming=true
micronaut:
views:
soy:
enabled: true
renaming: true
[micronaut]
[micronaut.views]
[micronaut.views.soy]
enabled=true
renaming=true
micronaut {
views {
soy {
enabled = true
renaming = true
}
}
}
{
micronaut {
views {
soy {
enabled = true
renaming = true
}
}
}
}
{
"micronaut": {
"views": {
"soy": {
"enabled": true,
"renaming": true
}
}
}
}
-
When building your styles with GSS, or a similar tool, generate a rewrite map:
> java -jar closure-stylesheets.jar \ --output-renaming-map-format JSON \ --output-renaming-map src/main/resources/renaming-map.json \ --rename CLOSURE \ [...inputs...] > src/main/resources/styles/app-styles.min.css
-
In your template sources, annotate CSS classes with
{css('name')}
calls. Note that the value passed into each call must be a string literal (variables cannot be used):{template .foo} <div class="{css('my-cool-class')} {css('my-cool-class-active')}"> ... content, and stuff... </div> {/template}
-
In your CSS, use the names you mentioned in your template:
.my-cool-class { color: blue; } .my-cool-class-active { background: yellow; }
-
Compile your templates (see: Building Soy).
> java -jar SoyToJbcSrcCompiler.jar \ --output templates.jar \ --srcs [...templates...];
The last step is to provide the renaming map to Micronaut Views Soy:
@Singleton
public class RewriteMapProvider implements SoyNamingMapProvider {
/** Filename for the JSON class renaming map. */
private static final String RENAMING_MAP_NAME = "renaming-map.json";
/** Naming map to use for classes. */
private static SoyCssRenamingMap CSS_RENAMING_MAP = null;
/**
* Load JSON embedded in a JAR resource into a naming map for Soy rendering.
*
* @param mapPath URL to the JAR resource we should load.
*/
@Nullable
private static Map<String, String> loadMapFromJSON(URL mapPath) {
try {
return new ObjectMapper().readValue(mapPath, new TypeReference<Map<String, String>>() { });
} catch (Throwable thr) {
//handle `JsonMappingException` and `IOException`, if, for instance, you're using Jackson
throw new RuntimeException(thr);
}
}
static {
final URL mapPath =
SoyRenderConfigProvider.class.getClassLoader().getResource(RENAMING_MAP_NAME);
if (mapPath != null) {
// load the renaming map
final Map<String, String> cssRenamingMap = loadMapFromJSON(mapPath);
if (renamingMapRaw != null) {
CSS_RENAMING_MAP = new SoyCssRenamingMap() {
@Nullable @Override
public String get(@NotNull String className) {
// (or whatever logic you need to rewrite the class)
return cssRenamingMap.get(className);
}
};
}
}
}
/**
* Provide a CSS renaming map to Soy/Micronaut.
*
* @return Inflated Soy CSS renaming map.
*/
@Nullable @Override public SoyCssRenamingMap cssRenamingMap() {
return CSS_RENAMING_MAP;
}
}
Then, your output will be renamed. Referencing the Soy template sample above, output would look something like this:
<div class="a-b-c a-b-c-d">... content, and stuff...</div>
With your CSS rewritten and minified to match:
.a-b-c{color:blue;}.a-b-c-d{background:yellow;}
3.1.6.5 Building Soy
Soy has tooling support from Node (Gulp, Grunt), Maven, Gradle, and Bazel. You can also invoke each Soy compiler directly via the runner classes for each one:
-
SoyHeaderCompiler: Compile templates into light "headers," that can be used downstream as dependencies for other templates.
-
SoyMsgExtractor: Enables easy i18n by extracting
{msg desc=""}{/msg}
declarations for processing or translation. -
SoyParseInfoGenerator: Generates template parser metadata information as Java sources.
-
SoyPluginValidator: Validates
SoySourceFunction
definitions found in a set of JARs. -
SoyToIncrementalDomSrcCompiler: Generate client-side templates that render with IncrementalDOM (AKA
idom
). This compiler uses direct calls into the DOM to incrementally render content, as opposed to the traditional client-side approach, which renders strings intoelement.innerHTML
. This support is currently experimental and involves a number of external dependencies. -
SoyToJbcSrcCompiler: Compiles Soy templates directly to Java bytecode, packaged up in a JAR. These templates can be used together with Soy Java for highly performant server-side rendering.
-
SoyToJsSrcCompiler: Traditional JS client-side compiler, which assembles strings of rendered template content. Like
idom
, these compiled templates work well with Closure Compiler, and Closure Library viagoog.soy
. -
SoyToPySrcCompiler: Compiles templates into Python sources for use server-side.
3.1.7 Pebble
Micronaut Views Pebble includes PebbleViewsRenderer which uses the Pebble project.
Add the following dependency on your classpath. For example, in build.gradle
implementation("io.micronaut.views:micronaut-views-pebble")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-pebble</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Pebble template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
{% if loggedIn %}
<h1>username: <span>{{ username }}</span></h1>
{% else %}
<h1>You are not logged in</h1>
{% endif %}
</body>
</html>
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether the component is enabled. Default value (true). |
|
java.lang.String |
The default extension. Default value ("html"). |
|
boolean |
Enable/disable all caches, i.e. cache used by the engine to store compiled PebbleTemplate instances and tags cache. Default value (true). |
|
boolean |
Changes the newLineTrimming setting of the PebbleEngine. By default, Pebble will trim a new line that immediately follows a Pebble tag. If set to false, then the first newline following a Pebble tag won’t be trimmed. All newlines will be preserved. Default value (true). |
|
boolean |
Sets whether or not escaping should be performed automatically. Default value (true). |
|
java.lang.String |
Sets the default escaping strategy of the built-in escaper extension. Default value (EscapeFilter.HTML_ESCAPE_STRATEGY). |
|
boolean |
Changes the strictVariables setting of the PebbleEngine. Default value (false). |
|
boolean |
Enable/disable greedy matching mode for finding java method. Default is disabled. If enabled, when can not find perfect method (method name, parameter length and parameter type are all satisfied), reduce the limit of the parameter type, try to find other method which has compatible parameter types. Default value (false). |
|
boolean |
Sets whether or not core operators overrides should be allowed. Default value (false). |
|
boolean |
Enable/disable treat literal decimal as Integer. Default value (false), treated as Long. |
|
boolean |
Enable/disable treat literal numbers as BigDecimals. Default value (false), treated as Long/Double. |
3.1.8 JTE
Micronaut Views Jte includes JteViewsRenderer which uses jte (Java Template Engine), a secure and lightweight template engine for Java and Kotlin.
Add the micronaut-views-jte
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-jte")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-jte</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Jte template:
@param Boolean loggedIn
@param String username
<html>
<head>
<title>Home</title>
</head>
<body>
@if (loggedIn != null && loggedIn)
<h1>username: <span>${username}</span></h1>
@else
<h1>You are not logged in</h1>
@endif
</body>
</html>
Compiling Templates
Jte templates may be precompiled at build time. This can be done by a Gradle plugin or Maven plugin. If not precompiled, the application will need a JDK so it can compile templates at runtime.
Dynamic Reloading
When dynamic
is enabled (see below), jte will load templates from the project source directory, and will reload them after changes.
Configuration
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
boolean |
Whether to enable dynamic reloading of templates. Default value (false). |
|
java.lang.String |
Root directory under which to write generated source and class files. . Default value ("build/jte-classes"). |
|
java.lang.String |
When using dynamic templates, the root source directory to search. If not specified, jte will search src/<sourceset>/jte and src/<sourceset>/resources/<folder> where 'folder' is ViewsConfiguration.getFolder(). In cases where the source directory cannot be found, jte will use classpath loading instead, and will not dynamically reload templates. |
|
boolean |
Enable building binary content for templates. Default value (false). (Only has an effect when 'dynamic' is true. To use with precompiled templates, enable it in the build plugin) |
.kte (Kotlin) templates
To use .kte
(Kotlin) templates include the dependency:.
implementation("gg.jte:jte-kotlin")
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-kotlin</artifactId>
</dependency>
3.1.9 JStachio
Micronaut Views JStachio includes JStachioMessageBodyWriter, which uses JStachio, a type-safe compiled Mustache-based template engine, to render templates.
3.1.9.1 JStachio Installation
To use Micronaut Jstachio, add the micronaut-views-jstachio
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-jstachio")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-jstachio</artifactId>
</dependency>
Additionally, you need to add JStachio’s Java annotation processor.
annotationProcessor("io.jstach:jstachio-apt")
<annotationProcessorPaths>
<path>
<groupId>io.jstach</groupId>
<artifactId>jstachio-apt</artifactId>
</path>
</annotationProcessorPaths>
Read the Jstachio’s user guide to learn more.
3.1.9.2 JStachio Example
With Micronaut JStachio, you cannot use the View annotation, respond in your controller a ModelAndView or use Micronaut Views Configuration to configure the views directory since it does not use the ViewsFilter. |
Instead, a controller returns a model annotated with @JStache
referencing the view
name.
@Controller("/views")
public class HomeController {
@Produces(MediaType.TEXT_HTML)
@Get
HomeModel index() {
return new HomeModel("sdelamo", true);
}
}
package io.micronaut.views.jstachio.pkginfo;
import io.jstach.jstache.JStache;
import io.micronaut.core.annotation.Nullable;
@JStache(path = "home")
public record HomeModel(@Nullable String username, boolean loggedIn) {
}
You can provide a suffix
and prefix
for every model with the @JStacheConfig
annotation.
Add a package-info.java
in the same package of your model classes.
@JStacheConfig(
pathing = @JStachePath(prefix = "views/", suffix = ".mustache")
)
package io.micronaut.views.jstachio.pkginfo;
import io.jstach.jstache.JStacheConfig;
import io.jstach.jstache.JStachePath;
The previous example renders the following template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
{{#loggedIn}}
<h1>username: <span>{{username}}</span></h1>
{{/loggedIn}}
{{^loggedIn}}
<h1>You are not logged in</h1>
{{/loggedIn}}
</body>
</html>
3.2 Working with Models
The Micronaut framework provides a simple way to modify and enhance a model prior to being sent to a template engine for rendering.
3.2.1 Dynamically Enriching Models
Provide custom processors by registering beans of type ViewModelProcessor.
The following example shows a bean of type ViewModelProcessor which includes a fictitious configuration object in the rendering context:
@Singleton (1)
public class ConfigViewModelProcessor implements ViewModelProcessor<Map<String, Object>> {
private final ApplicationConfiguration config;
ConfigViewModelProcessor(ApplicationConfiguration environment) {
this.config = environment;
}
@Override
public void process(@NonNull HttpRequest<?> request,
@NonNull ModelAndView<Map<String, Object>> modelAndView) {
modelAndView.getModel()
.ifPresent(model -> {
if (config.getName().isPresent()) {
model.put("applicationName", config.getName().get());
}
});
}
}
1 | Use javax.inject.Singleton to designate this class as a singleton in the Micronaut Bean Context. |
If you use Micronaut Security, Micronaut Views registers a ViewModelProcessor to enrich models with security related information.
3.3 Fieldset Generation
Fieldset API is experimental and subject to change. |
The FieldsetGenerator API simplifies the generation of an HTML Fieldset representation for a given type or instance. It leverages the introspection builder support.
The FormGenerator API wraps the previous API and simplifies the generation of an HTML form.
To use these APIs, you need the dependency the following dependency:
implementation("io.micronaut.views:micronaut-views-fieldset")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-fieldset</artifactId>
</dependency>
3.3.1 Form Generation Example
Imagine, you want to create an application which displays a form such as:
<form action="/books/save" method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" name="title" value="" id="title" minlength="2" maxlength="255" class="form-control" required="required"/>
</div>
<div class="mb-3">
<label for="pages" class="form-label">Pages</label>
<input type="number" name="pages" value="" id="pages" min="1" max="21450" class="form-control" required="required"/>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
In Java, you will create a representation for the form submission. Something like:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Serdeable
public record BookSave(@Size(min = 2, max = 255) @NotBlank String title,
@Min(1) @Max(21450) @NotNull Integer pages) {
}
The field types and the validation annotations in the previous code sample influence the form generation. |
Then using the form generation API, create a controller such as:
@Controller("/books")
class BookController {
public static final String CONTROLLER_PATH = "/books";
public static final String SAVE_PATH = "/save";
public static final String FORM = "form";
private final FormGenerator formGenerator;
private final BookRepository bookRepository;
BookController(FormGenerator formGenerator,
BookRepository bookRepository) {
this.formGenerator = formGenerator;
this.bookRepository = bookRepository;
}
@Produces(MediaType.TEXT_HTML)
@View("/books/create.html")
@Get("/create")
Map<String, Object> create() {
return Collections.singletonMap(FORM,
formGenerator.generate(CONTROLLER_PATH + SAVE_PATH, BookSave.class));
}
@Produces(MediaType.TEXT_HTML)
@View("/books/list.html")
@Get("/list")
Map<String, Object> list() {
return Collections.singletonMap("books", bookRepository.findAll());
}
@Produces(MediaType.TEXT_HTML)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post(SAVE_PATH)
HttpResponse<?> save(@Valid @Body BookSave bookSave) {
bookRepository.save(new Book(null, bookSave.title(), bookSave.pages()));
return HttpResponse.seeOther(UriBuilder.of("/books").path("list").build());
}
@Error(exception = ConstraintViolationException.class)
public HttpResponse<?> onConstraintViolationException(HttpRequest<?> request, ConstraintViolationException ex) {
if (request.getPath().equals(CONTROLLER_PATH + SAVE_PATH)) {
Optional<BookSave> bookSaveOptional = request.getBody(BookSave.class);
if (bookSaveOptional.isPresent()) {
Form form = formGenerator.generate(CONTROLLER_PATH + SAVE_PATH, bookSaveOptional.get(), ex);
ModelAndView<Map<String, Object>> body = new ModelAndView<>("/books/create.html",
Collections.singletonMap(FORM, form));
return HttpResponse.unprocessableEntity().body(body);
}
}
return HttpResponse.serverError();
}
}
Micronaut Launch or the Micronaut Command Line Interface (CLI) will generate Thymeleaf fragments to render a form when you select the views-thymeleaf
feature.
Thanks to those fragments, rendering the form for the /books/create
route in the previous example is really simple:
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::title},~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title></title>
<script></script>
</head>
<body>
<main>
<form th:replace="~{fieldset/form :: form(${form})}"></form>
</main>
</body>
</html>
3.3.2 Fieldset Annotations
Sometimes, more than the Java type is needed to define the input type. For example, you may want to render a login form such as:
<form action="/login" method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" name="username" value="" id="username" class="form-control" required="required"/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" value="" id="password" class="form-control" required="required"/>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
In Java, you will create a representation for the form submission and annotate the password
field with @InputPassword. Something like:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.views.fields.annotations.InputPassword;
import jakarta.validation.constraints.NotBlank;
@Introspected
public record Login(@NotBlank String username,
@InputPassword @NotBlank String password) {
}
The following annotations are available:
Annotation | Description |
---|---|
Annotation to specify a field is a checkbox input. |
|
Annotation to specify a field is an email input. |
|
Annotation to specify a field is a hidden input. |
|
Annotation to specify a field is a number input. |
|
Annotation to specify a field is a password input. |
|
Annotation to specify a field is a radio input. |
|
Annotation to specify a field is a telephone input. |
|
Annotation to specify a field is a text input. |
|
Annotation to specify a field is an url input. |
|
Annotation to specify a field is an HTML select element. |
|
Annotation to specify a field is a textarea. |
|
Annotation to mark a field as a Trix editor. |
3.3.3 Radio, Checkbox and Option Fetcher
The @InputCheckbox, @InputRadio, and @Select annotations allow you to specify a fetcher class to load data necessary for the form.
Imagine you want a form to associate an author with a book form, such as:
<form action="/books/authors/save" method="post">
<input type="hidden" name="bookId" value="1"/>
<div class="mb-3">
<label for="authorId" class="form-label">Author Id</label>
<select name="authorId" id="authorId" class="form-select" required="required">
<option value="1">Kishori Sharan</option>
<option value="2">Peter Späth</option>
<option value="3">Sam Newman</option>
</select>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>"
In Java, you will create a representation for the form submission. Something like:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.views.fields.annotations.InputHidden;
import io.micronaut.views.fields.annotations.Select;
import jakarta.validation.constraints.NotNull;
@Serdeable
public record BookAuthorSave(@NotNull @InputHidden Long bookId,
@NotNull @Select(fetcher = AuthorFetcher.class) Long authorId) {
}
The fetcher
member of the Select
annotation allows you to specify a class. AuthorFetcher
is a Singleton
of type OptionFetcher
, and you could write it, for example, like this:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.views.fields.messages.Message;
import io.micronaut.views.fields.elements.Option;
import io.micronaut.views.fields.fetchers.OptionFetcher;
import jakarta.inject.Singleton;
import java.util.List;
@Singleton
public class AuthorFetcher implements OptionFetcher<Long> {
private final AuthorRepository authorRepository;
public AuthorFetcher(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@Override
public List<Option> generate(Class<Long> type) {
return authorRepository.findAll()
.stream()
.map(author -> Option.builder()
.value(String.valueOf(author.id()))
.label(Message.of(author.title()))
.build()
).toList();
}
@Override
public List<Option> generate(Long instance) {
return authorRepository.findAll()
.stream()
.map(author -> Option.builder()
.selected(author.id().equals(instance))
.value(String.valueOf(author.id()))
.label(Message.of(author.title()))
.build())
.toList();
}
}
3.3.4 Custom Form Elements
You many need a custom implementation of FormElement. If you do, create a bean of type FormElementResolver to resolve your custom FormElement
class for a certain bean property’s type.
Providing a bean of type FormElementResolver allows the custom form element to be part of the FieldsetGenerator API.
3.4 HTMX
To integrate with HTMX, add the following dependency:
implementation("io.micronaut.views:micronaut-views-htmx")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-htmx</artifactId>
</dependency>
3.4.1 HTMX Request Headers
You can bind HtmxRequestHeaders in a controller method.
In the following example, the parameter is bound if the request is an HTMX request. If it is not an HTMX request, the parameter is bound as null.
@Get
ModelAndView<Map<String, Object>> index(@Nullable HtmxRequestHeaders htmxRequestHeaders) {
Map<String, Object> model = Collections.singletonMap("fruit", new Fruit("Apple", "Red"));
if (htmxRequestHeaders != null) {
return new ModelAndView<>("fruit", model);
}
return new ModelAndView<>("fruits", model);
}
@Get
ModelAndView<Map<String, Object>> index(@Nullable HtmxRequestHeaders htmxRequestHeaders) {
Map<String, Object> model = [fruit: new Fruit("Apple", "Red")]
if (htmxRequestHeaders != null) {
return new ModelAndView<>("fruit", model)
}
new ModelAndView<>("fruits", model)
}
@Get
fun index(htmxRequestHeaders: HtmxRequestHeaders?): ModelAndView<Map<String, Any>> {
val model = mapOf("fruit" to Fruit("Apple", "Red"))
if (htmxRequestHeaders != null) {
return ModelAndView("fruit", model)
}
return ModelAndView("fruits", model)
}
3.4.2 Out of Band Swaps
You can return an API:views.htmx.HtmxResponse[] in a controller method to render multiple views in a single HTMX response—for example, to do Out Of Band Swaps.
@Post
HtmxResponse<?> outOfBandSwaps(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
return HtmxResponse.builder()
.modelAndView(new ModelAndView<>("fruit", Collections.singletonMap("fruit", new Fruit("Apple", "Red"))))
.modelAndView(new ModelAndView<>("swap", Collections.emptyMap()))
.build();
}
@Post
HtmxResponse<?> outOfBandSwaps(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
HtmxResponse.builder()
.modelAndView(new ModelAndView<>("fruit", [fruit: new Fruit("Apple", "Red")]))
.modelAndView(new ModelAndView<>("swap", [:]))
.build()
}
@Post
fun outOfBandSwaps(htmxRequestHeaders: HtmxRequestHeaders): HtmxResponse<*> {
return HtmxResponse.builder<Any>()
.modelAndView(ModelAndView("fruit", mapOf("fruit" to Fruit("Apple", "Red"))))
.modelAndView(ModelAndView("swap", emptyMap<Any, Any>()))
.build()
}
3.4.3 HTMX Response Headers
The class HtmxResponseHeaders defines constants for the HTMX Response headers.
@Get("/responseHeaders")
HttpResponse<?> htmxResponseHeaders(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
return HttpResponse.ok().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE);
}
@Get("/responseHeaders")
HttpResponse<?> htmxResponseHeaders(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
HttpResponse.ok().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE);
}
@Get("/responseHeaders")
fun htmxResponseHeaders(htmxRequestHeaders: @NonNull HtmxRequestHeaders): HttpResponse<*> {
return HttpResponse.ok<Any>().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE)
}
3.5 Turbo
A set of complementary techniques for speeding up page changes and form submissions, dividing complex pages into components, and stream partial page updates over WebSocket. All without writing any JavaScript at all. And designed from the start to integrate perfectly with native hybrid applications for iOS and Android.
3.5.1 TurboFrameView annotation
Turbo fetch request includes an HTTP Header Turbo-Frame
when you click a link within a Turbo Frame.
For example, for a frame id message_1
and a response such as:
<!DOCTYPE html>
<html>
<head>
<title>Edit</title>
</head>
<body>
<h1>Editing message</h1>
<turbo-frame id="message_1">
<form action="/messages/1">
<input name="name" type="text" value="My message title">
<textarea name="content">My message content</textarea>
<input type="submit">
</form>
</turbo-frame>
</body>
</html>
Turbo extracts <turbo-frame id="message_1">
and it replaces the frame content from where the click originated. When Turbo updates the frame, it uses only content inside a matching <turbo-frame>
.
You can render a Turbo Frame easily by annotating a controller route with TurboFrameView and returning only the HTML used by Turbo.
<turbo-frame id="message_1">
<form action="/messages/1">
<input name="name" type="text" value="My message title">
<textarea name="content">My message content</textarea>
<input type="submit">
</form>
</turbo-frame>
The following example illustrates this behaviour with two Velocity templates and a controller.
<!DOCTYPE html>
<html>
<head>
<title>Edit</title>
</head>
<body>
<h1>Editing message</h1>
<turbo-frame id="message_$message.getId()">
#parse("views/form.vm")
</turbo-frame>
</body>
</html>
<form action="/messages/$message.getId()">
<input name="name" type="text" value="$message.getName()">
<textarea name="content">$message.getContent()</textarea>
<input type="submit">
</form>
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
Map<String, Object> index() {
return Map.of("message", new Message(1L, "My message title", "My message content"));
}
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
Map<String, Object> index() {
["message": new Message(id: 1L, name: "My message title", content: "My message content")]
}
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
fun index() =
mapOf("message" to Message(1L, "My message title", "My message content"))
3.5.2 TurboFrame Builder
You can render Turbo Frames also by returning TurboFrame.Builder
from a controller.
@Produces(MediaType.TEXT_HTML)
@Get("/builder")
HttpResponse<?> index(@Nullable @Header(TurboHttpHeaders.TURBO_FRAME) String turboFrame) {
Long messageId = 1L;
Map<String, Object> model = Map.of(
"message", new Message(messageId, "My message title", "My message content")
);
return HttpResponse.ok(turboFrame == null ?
new ModelAndView<>("edit", model) :
TurboFrame.builder()
.id("message_" + messageId)
.templateModel(model)
.templateView("form"))
.contentType(MediaType.TEXT_HTML);
}
@Produces(MediaType.TEXT_HTML)
@Get("/builder")
HttpResponse<?> index(@Nullable @Header(TurboHttpHeaders.TURBO_FRAME) String turboFrame) {
Long messageId = 1L
Map<String, Object> model = [
"message": new Message(id: messageId, name: "My message title", content: "My message content")
]
HttpResponse.ok(turboFrame == null ? new ModelAndView("edit", model) :
TurboFrame.builder()
.id("message_" + messageId)
.templateModel(model)
.templateView("form")
).contentType(MediaType.TEXT_HTML)
}
@Produces(MediaType.TEXT_HTML)
@Get("/builder")
fun index(@Header(TurboHttpHeaders.TURBO_FRAME) turboFrame: String?): HttpResponse<*> {
val messageId = 1L
val model = mapOf("message" to Message(messageId, "My message title", "My message content"))
return HttpResponse.ok(
if (turboFrame == null)
ModelAndView<Any?>("edit", model)
else
TurboFrame.builder()
.id("message_$messageId")
.templateModel(model)
.templateView("form")
).contentType(MediaType.TEXT_HTML)
}
3.5.3 Turbo Stream Fluid API
You can render Turbo Streams by returning a TurboStream.Builder
from a controller.
TurboStream
features a fluid API:
TurboStream turboStream = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template("Content to append to container designated with the dom_id.")
.build();
Optional<Writable> writable = turboStream.render();
TurboStream turboStream = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template("Content to append to container designated with the dom_id.")
.build()
Optional<Writable> writable = turboStream.render();
val turboStream = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template("Content to append to container designated with the dom_id.")
.build()
val writable = turboStream.render()
The previous example generates:
"<turbo-stream action=\"append\" target=\"dom_id\">" +
"<template>" +
"Content to append to container designated with the dom_id." +
"</template>" +
"</turbo-stream>"
'<turbo-stream action="append" target="dom_id">' +
'<template>' +
'Content to append to container designated with the dom_id.' +
'</template>' +
'</turbo-stream>'
"<turbo-stream action=\"append\" target=\"dom_id\">" +
"<template>" +
"Content to append to container designated with the dom_id." +
"</template>" +
"</turbo-stream>"
3.5.4 TurboView Annotation
You can render Turbo Streams easily by annotating a controller route with TurboView
@Produces(value = {MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM})
@TurboView(value = "fruit", action = TurboStreamAction.APPEND)
@Get("/turbofruit")
Map<String, Object> show() {
return Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"));
}
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@TurboView(value = "fruit", action = TurboStreamAction.APPEND)
@Get("/turbofruit")
Map<String, Object> show() {
return Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"));
}
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@TurboView(value = "fruit", action = TurboStreamAction.APPEND)
@Get("/turbofruit")
fun show() = mapOf("fruit" to Fruit("Banana", "Yellow"))
Given the HTTP Request:
HttpRequest<?> request = HttpRequest.GET("/turbofruit")
.accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML)
.header(TurboHttpHeaders.TURBO_FRAME, "dom_id");
HttpRequest<?> request = HttpRequest.GET("/turbofruit")
.accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML)
.header(TurboHttpHeaders.TURBO_FRAME, "dom_id");
val request: HttpRequest<*> = HttpRequest.GET<Any>("/turbofruit")
.accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML)
.header(TurboHttpHeaders.TURBO_FRAME, "dom_id")
The previous controller returns:
"<turbo-stream action=\"append\" target=\"dom_id\">"+
"<template>" +
"<h1>fruit: Banana</h1>\n" +
"<h2>color: Yellow</h2>\n" +
"</template>" +
"</turbo-stream>"
'<turbo-stream action="append" target="dom_id">' +
'<template>' +
'<h1>fruit: Banana</h1>\n' +
'<h2>color: Yellow</h2>\n' +
'</template>' +
'</turbo-stream>'
"<turbo-stream action=\"append\" target=\"dom_id\">" +
"<template>" +
"<h1>fruit: Banana</h1>\n" +
"<h2>color: Yellow</h2>\n" +
"</template>" +
"</turbo-stream>"
with content type text/vnd.turbo-stream.html
.
3.5.5 Turbo Stream Media Type
You can use TurboMediaType to evaluate whether a request accepts Turbo Streams.
When submitting a <form> element whose method attribute is set to POST, PUT, PATCH, or DELETE, Turbo injects text/vnd.turbo-stream.html into the set of response formats in the request’s Accept header.
An example of handling a multipart form POST with Turbo and the Micronaut Framework would be:
@Consumes(MediaType.MULTIPART_FORM_DATA)
@TurboView("view")
@Produces(value = {MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM})
@Post("/messages/{id}")
Map<String, Object> processEdit(@Part int id, @Part String title, @Part String body) {
// Process the posted data, and return the updated message
return Map.of("message", new Message((long) id, title, body));
}
@Consumes(MediaType.MULTIPART_FORM_DATA)
@TurboView("view")
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@Post("/messages/{id}")
Map<String, Object> processEdit(@Part int id, @Part String title, @Part String body) {
// Process the posted data, and return the updated message
[message: new Message((long) id, title, body)]
}
@Consumes(MediaType.MULTIPART_FORM_DATA)
@TurboView("view")
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@Post("/messages/{id}")
fun processEdit(@Part id: Int, @Part title: String, @Part body: String): Map<String, Any> {
// Process the posted data, and return the updated message
return mapOf("message" to Message(id.toLong(), title, body))
}
3.5.6 Turbo Streams with Templates
You can use TurboStreamRenderer to render Turbo Streams with server side templates.
String view = "fruit";
Map<String, Object> model = Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"));
TurboStream.Builder builder = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template(view, model);
Optional<Writable> writable = turboStreamRenderer.render(builder, null);
String view = "fruit"
Map<String, Object> model = Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"))
TurboStream.Builder builder = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template(view, model)
Optional<Writable> writable = turboStreamRenderer.render(builder, null)
val view = "fruit"
val model = mapOf("fruit" to Fruit("Banana", "Yellow"))
val builder = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template(view, model)
val writable = turboStreamRenderer.render(builder, null)
3.5.7 Guides
See the following guides to learn more about working with Turbo views in Micronaut Framework.
3.6 Security
3.6.1 Content Security Policy
Micronaut Views supports CSP (Content Security Policy, Level 3) out of the box. By default, CSP is disabled. To enable CSP, modify your configuration. For example:
micronaut.views.csp.enabled=true
micronaut:
views:
csp:
enabled: true
[micronaut]
[micronaut.views]
[micronaut.views.csp]
enabled=true
micronaut {
views {
csp {
enabled = true
}
}
}
{
micronaut {
views {
csp {
enabled = true
}
}
}
}
{
"micronaut": {
"views": {
"csp": {
"enabled": true
}
}
}
}
See the following table for all configuration options:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether CSP is enabled. Default value (false). |
|
java.lang.String |
Sets the policy directives. |
|
boolean |
If true, the Content-Security-Policy-Report-Only header will be sent instead of Content-Security-Policy. Default value (false). |
|
java.util.Random |
The |
|
boolean |
If true, the CSP header will contain a generated nonce that is made available to view renderers. The nonce should change for each request/response cycle and can be used by views to authorize inlined script blocks. |
|
boolean |
Sets whether |
|
java.lang.String |
The path the CSP filter should apply to. Default value ("/**"). |
Nonce Support
At the developer’s option, Micronaut Views can generate a nonce with each render cycle. This nonce value can be used in
script-src
and style-src
directives in a CSP response header (note that nonce
values generally do not have any
effect when Content Security Policy is set via a <meta http-equiv>
tag).
To opt-in to this behavior, configure Micronaut Views with generateNonce
set to true
. Additionally, provide a spot for the
nonce value in your CSP directives, with the token {#nonceValue}
. It must be preceded by nonce-
and wrapped in
single quotes, as per the CSP3 spec:
micronaut.views.csp.enabled=true
micronaut.views.csp.generateNonce=true
micronaut.views.csp.policyDirectives=default-src https: self:; script-src 'nonce-{#nonceValue}';
micronaut:
views:
csp:
enabled: true
generateNonce: true
policyDirectives: "default-src https: self:; script-src 'nonce-{#nonceValue}';"
[micronaut]
[micronaut.views]
[micronaut.views.csp]
enabled=true
generateNonce=true
policyDirectives="default-src https: self:; script-src \'nonce-{#nonceValue}\';"
micronaut {
views {
csp {
enabled = true
generateNonce = true
policyDirectives = "default-src https: self:; script-src 'nonce-{#nonceValue}';"
}
}
}
{
micronaut {
views {
csp {
enabled = true
generateNonce = true
policyDirectives = "default-src https: self:; script-src 'nonce-{#nonceValue}';"
}
}
}
}
{
"micronaut": {
"views": {
"csp": {
"enabled": true,
"generateNonce": true,
"policyDirectives": "default-src https: self:; script-src 'nonce-{#nonceValue}';"
}
}
}
}
That’s it! After applying the above configuration, HTTP responses might include a header that look like this:
Content-Security-Policy: default-src https: self:; script-src 'nonce-4ze2IRazk4Yu/j5K6SEzjA';
Inline scripts which aren’t otherwise whitelisted will be declined for execution, unless CSP is operating in report-only mode. Inline scripts can be whitelisted with the syntax:
<script type="text/javascript" nonce="4ze2IRazk4Yu/j5K6SEzjA">
// some javascript code here
</script>
3.6.2 Integration with Micronaut Security
The views project has integration with the Micronaut security project.
SecurityViewModelProcessor is enabled by default and injects the current username in the view.
SecurityViewModelProcessor is typed to Map<String, Object> . If are models are POJOs you will need to implement your own security ViewModelProcessor.
|
The following properties allow you to customize the injection:
Property | Type | Description |
---|---|---|
|
boolean |
Enable {@link SecurityViewModelProcessor}. Default value (true). |
|
java.lang.String |
Model key name. Default value ("security"). |
|
java.lang.String |
Nested security map key for the user’s name property. Default value ("name"). |
|
java.lang.String |
Nested security map key for the user’s attributes property. Default value ("attributes"). |
In a controller, you can return a model without specifying in the model the authenticated user:
@Controller("/")
public class BooksController {
@Secured(SecurityRule.IS_AUTHENTICATED)
@View("securitydecorator")
@Get
public Map<String, Object> index() {
Map<String, Object> model = new HashMap<>();
model.put("books", Arrays.asList("Developing Microservices"));
return model;
}
}
and still access the authenticated user in the view (for example a velocity template):
<!DOCTYPE html>
<html>
<head>
<title>User Books</title>
</head>
<body>
#if( $security )
<h1>User: ${security['name']} email: ${security['attributes']['email']}</h1>
#end
#foreach($book in $books)
$book
#end
#if( $securitycustom )
<h1>Custom: ${securitycustom['name']}</span></h1>
#end
</body>
</html>
You can access information about the current user with the security
map.
3.7 GraalVM Support
The Micronaut framework supports GraalVM and starting in 1.2.0
special configuration files have been added to make easier the native image creation.
For Micronaut framework 2.0 the support for GraalVM has been improved and now it’s not necessary to declare any view.
This also applies if you serve static resources like
html, css and javascript files. Everything in the public
directory will also be added to the GraalVM resource configuration
automatically.
Learn more about Micronaut framework’s GraalVM support. |
4 Guides
See the following list of guides to learn more about working with Views in the Micronaut Framework:
5 Breaking Changes
This section outlines the breaking changes done in major versions of Micronaut Views.
4.0.0
-
The Soy dependency has been upgraded to version 2022-10-26, which includes breaking changes. Among others,
.
is no longer allowed before template names. To know the full changes, we suggest looking at the commits since there exist no release notes. -
To reduce the size of applications where Http requests are not required, the core library has been detached from the micronaut http library and no longer declares it as an api dependency. This change has added another Generic type to ViewRenderer, and breaks binary compatibility.
3.0.0
-
ViewModelProcessor no longer assumes a
Map<String,Object>
model and must be typed to the exact type of the model you would like to process. -
ViewsRenderer are now typed. Moreover, provided
ViewsRenderer
don’t specify@Produces(MediaType.TEXT_HTML)
and responses content type respect the content type defined for the route. -
Method
ViewsRenderer::render(String, T)
was removed. UseViewsRenderer::render(String, T, HttpRequest)
instead. -
Decoration of view model is now handled by ViewsModelDecorator and its default implementation DefaultViewsModelDecorator
-
Resolution of view name is now handled by ViewsResolver and its default implementation DefaultViewsResolver
-
Resolution of ViewsRenderer used to render a view is now handled by ViewsRendererLocator and its default implementation DefaultViewsRendererLocator
-
ViewsRenderer::modelOf
method has been moved toViewUtils::modelOf
-
Constant
EXTENSION_SEPARATOR
has been moved fromViewsRenderer
toViewUtils
2.0.0
-
The
micronaut-views
dependency is no longer published. Replace the dependency with the one specific to the view implementation being used. -
Deprecated classes, constructors, etc have been removed.
-
ViewsFilterOrderProvider has been removed. The view filter now always runs on
ServerFilterPhase.RENDERING
. -
Maven Group ID changed to
io.micronaut.views
6 Repository
You can find the source code of this project in this repository: