Micronaut Views

Provides integration between Micronaut and server-side views technologies

Version:

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.

src/main/java/myapp/ViewsController.java
@Controller("/views")
class ViewsController {

    @View("home")
    @Get("/")
    public HttpResponse<?> index() {
        return HttpResponse.ok(CollectionUtils.mapOf("loggedIn", true, "username", "sdelamo"))
    }

}
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:

src/main/java/myapp/ViewsController.java
@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.

src/main/java/myapp/ViewsController.java
@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");
    }

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:

🔗
Table 1. Configuration Properties for ThymeleafViewsRendererConfigurationProperties
Property Type Description

micronaut.views.thymeleaf.character-encoding

java.lang.String

micronaut.views.thymeleaf.template-mode

org.thymeleaf.templatemode.TemplateMode

micronaut.views.thymeleaf.suffix

java.lang.String

micronaut.views.thymeleaf.force-suffix

boolean

micronaut.views.thymeleaf.force-template-mode

boolean

micronaut.views.thymeleaf.cache-ttlms

long

micronaut.views.thymeleaf.check-existence

boolean

micronaut.views.thymeleaf.cacheable

boolean

micronaut.views.thymeleaf.enabled

boolean

enabled getter.

micronaut.views.thymeleaf.cache-ttl

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:

src/main/resources/views/home.html
<!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:

src/main/resources/views/layoutFile.html
<!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:

src/main/resources/views/home.hbs
<!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:

src/main/resources/views/home.vm
<!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:

src/main/resources/views/home.ftl
<!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:

src/main/resources/views/home.rocker.html
@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.

build.gradle
plugins {
    id "com.fizzed.rocker" version "1.2.3"
}

sourceSets {
    main {
        rocker {
            srcDir('src/main/resources')
        }
    }
}
pom.xml
<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:

🔗
Table 1. Configuration Properties for RockerViewsRendererConfigurationProperties
Property Type Description

micronaut.views.rocker.enabled

boolean

Enabled getter.

micronaut.views.rocker.default-extension

java.lang.String

micronaut.views.rocker.hot-reloading

boolean

Hot reloading getter.

micronaut.views.rocker.relaxed

boolean

Relaxed binding getter.

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:

🔗
Table 1. Configuration Properties for SoyViewsRendererConfigurationProperties
Property Type Description

micronaut.views.soy.enabled

boolean

Whether Soy-backed views are enabled. Default value true

micronaut.views.soy.renaming-enabled

boolean

Specifies whether renaming is enabled. Defaults to 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:

src/main/resources/views/home.soy
{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(val resourceLoader: ResourceLoader, viewsConfiguration: ViewsConfiguration) :
    SoyFileSetProvider {
    private val folder: String
    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"
        )
    }

    init {
        folder = viewsConfiguration.folder
    }
}
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<*> {
        return HttpResponse.ok(CollectionUtils.mapOf("loggedIn", true, "username", "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, with SoyToJbcSrcCompiler. If compiled templates can’t be located by the SoyFileSetProvider, 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 script src. To inject resources, use uri:

    /* 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 a string. 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:

  1. 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
      }
    }
  }
}
  1. 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
  2. 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}
  3. In your CSS, use the names you mentioned in your template:

    .my-cool-class {
      color: blue;
    }
    .my-cool-class-active {
      background: yellow;
    }
  4. 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 into element.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 via goog.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:

src/main/resources/views/home.html
<!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:

🔗
Table 1. Configuration Properties for PebbleConfigurationProperties
Property Type Description

micronaut.views.pebble.enabled

boolean

Sets whether the component is enabled. Default value (true).

micronaut.views.pebble.default-extension

java.lang.String

The default extension. Default value ("html").

micronaut.views.pebble.cache-active

boolean

Enable/disable all caches, i.e. cache used by the engine to store compiled PebbleTemplate instances and tags cache. Default value (true).

micronaut.views.pebble.new-line-trimming

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).

micronaut.views.pebble.auto-escaping

boolean

Sets whether or not escaping should be performed automatically. Default value (true).

micronaut.views.pebble.default-escaping-strategy

java.lang.String

Sets the default escaping strategy of the built-in escaper extension. Default value (EscapeFilter.HTML_ESCAPE_STRATEGY).

micronaut.views.pebble.strict-variables

boolean

Changes the strictVariables setting of the PebbleEngine. Default value (false).

micronaut.views.pebble.greedy-match-method

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).

micronaut.views.pebble.allow-override-core-operators

boolean

Sets whether or not core operators overrides should be allowed. Default value (false).

micronaut.views.pebble.literal-decimals-as-integers

boolean

Enable/disable treat literal decimal as Integer. Default value (false), treated as Long.

micronaut.views.pebble.literal-numbers-as-big-decimals

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:

src/main/resources/views/home.jte
@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:

🔗
Table 1. Configuration Properties for JteViewsRendererConfigurationProperties
Property Type Description

micronaut.views.jte.dynamic

boolean

Whether to enable dynamic reloading of templates. Default value (false).

micronaut.views.jte.dynamic-path

java.lang.String

Root directory under which to write generated source and class files. . Default value ("build/jte-classes").

micronaut.views.jte.dynamic-source-path

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.

micronaut.views.jte.binary-static-content

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.

src/main/java/io/micronaut/views/jstachio/pkginfo/HomeController.java
@Controller("/views")
public class HomeController {
    @Produces(MediaType.TEXT_HTML)
    @Get
    HomeModel index() {
        return new HomeModel("sdelamo", true);
    }
}
src/main/java/io/micronaut/views/jstachio/pkginfo/HomeModel.java
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.

src/main/java/io/micronaut/views/jstachio/pkginfo/package-info.java
@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:

src/main/resources/views/home.mustache
<!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:

src/main/java/myapp/model/ConfigViewModelProcessor.java
@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:

src/main/resources/views/books/create.html
<!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

@InputCheckbox

Annotation to specify a field is a checkbox input.

@InputEmail

Annotation to specify a field is an email input.

@InputHidden

Annotation to specify a field is a hidden input.

@InputNumber

Annotation to specify a field is a number input.

@InputPassword

Annotation to specify a field is a password input.

@InputRadio

Annotation to specify a field is a radio input.

@InputTel

Annotation to specify a field is a telephone input.

@InputText

Annotation to specify a field is a text input.

@InputUrl

Annotation to specify a field is an url input.

@Select

Annotation to specify a field is an HTML select element.

@Textarea

Annotation to specify a field is a textarea.

@TrixEditor

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 Turbo

Micronaut Views simplifies working with Turbo, the heart of Hotwire (HTML over the wire):

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.4.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.

src/main/resources/views/edit.vm
<!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>
src/main/resources/views/form.vm
<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 Collections.singletonMap("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(): Map<String, Any> {
            return mapOf("message" to Message(1L, "My message title", "My message content"))
        }

3.4.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 = Collections.singletonMap("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(@Nullable @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.4.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.4.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 TurboStreamTemplateTest.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(): Map<String, Any> {
    return Collections.singletonMap("fruit", 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>" +
    "</template>" +
"</turbo-stream>"
"<turbo-stream action=\"append\" target=\"dom_id\">"+
    "<template>" +
        "<h1>fruit: Banana</h1>\n" +
        "<h2>color: Yellow</h2>" +
    "</template>" +
"</turbo-stream>"
"<turbo-stream action=\"append\" target=\"dom_id\">" +
    "<template>" +
        "<h1>fruit: Banana</h1>\n" +
        "<h2>color: Yellow</h2>" +
    "</template>" +
"</turbo-stream>"

with content type text/vnd.turbo-stream.html.

3.4.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.

3.4.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 = Collections.singletonMap<String, Any>("fruit", Fruit("Banana", "Yellow"))
val builder = TurboStream.builder()
    .action(TurboStreamAction.APPEND)
    .targetDomId("dom_id")
    .template(view, model)
val writable = turboStreamRenderer.render(builder, null)

3.4.7 Guides

See the following guides to learn more about working with Turbo views in Micronaut Framework.

3.5 Security

3.5.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:

application configuration
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:

🔗
Table 1. Configuration Properties for CspConfiguration
Property Type Description

micronaut.views.csp.enabled

boolean

micronaut.views.csp.policy-directives

java.lang.String

micronaut.views.csp.report-only

boolean

micronaut.views.csp.random-engine

java.util.Random

micronaut.views.csp.generate-nonce

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.

micronaut.views.csp.force-secure-random

boolean

Sets whether SecureRandom is forced for use in generated nonce values. Defaults to false. Enabling this requires careful consideration, because SecureRandom will block infinitely without enough entropy.

micronaut.views.csp.filter-path

java.lang.String

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:

application configuration
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.5.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:

🔗
Table 1. Configuration Properties for SecurityViewModelProcessorConfigurationProperties
Property Type Description

micronaut.security.views-model-decorator.enabled

boolean

Enable {@link SecurityViewModelProcessor}. Default value (true).

micronaut.security.views-model-decorator.security-key

java.lang.String

micronaut.security.views-model-decorator.principal-name-key

java.lang.String

micronaut.security.views-model-decorator.attributes-key

java.lang.String

In a controller, you can return a model without specifying in the model the authenticated user:

src/main/java/myapp/BooksController.java
@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):

src/main/resources/views/books.vm
<!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.6 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.

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. Use ViewsRenderer::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 to ViewUtils::modelOf

  • Constant EXTENSION_SEPARATOR has been moved from ViewsRenderer to ViewUtils

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: