Micronaut Views

Provides integration between Micronaut and server-side views technologies

Version: 5.6.0-SNAPSHOT

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");
    }
    @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:

🔗
Table 1. Configuration Properties for ThymeleafViewsRendererConfigurationProperties
Property Type Description

micronaut.views.thymeleaf.character-encoding

java.lang.String

The character encoding to use. Default value ("UTF-8").

micronaut.views.thymeleaf.template-mode

org.thymeleaf.templatemode.TemplateMode

The template mode to be used.

micronaut.views.thymeleaf.suffix

java.lang.String

The suffix to use. Default value (".html").

micronaut.views.thymeleaf.force-suffix

boolean

Sets whether to force the suffix. Default value (false).

micronaut.views.thymeleaf.force-template-mode

boolean

Whether to force template mode. Default value (false).

micronaut.views.thymeleaf.cache-ttlms

long

The cache TTL in millis.

micronaut.views.thymeleaf.check-existence

boolean

Whether templates should be checked for existence.

micronaut.views.thymeleaf.cacheable

boolean

Whether templates are cacheable.

micronaut.views.thymeleaf.enabled

boolean

Whether thymeleaf rendering is enabled. Default value (true).

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

Whether Rocker views are enabled. Default value (true).

micronaut.views.rocker.default-extension

java.lang.String

The default extension to use for Rocker templates. Default value ("rocker.html").

micronaut.views.rocker.hot-reloading

boolean

Whether hot reloading is enabled. Default value (false).

micronaut.views.rocker.relaxed

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:

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

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(
    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, 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>

If you want to write your views in Kotlin, you can include an additional dependency:

implementation("gg.jte:jte-kotlin")
<dependency>
    <groupId>gg.jte</groupId>
    <artifactId>jte-kotlin</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>

For Kotlin, add the jstachio-apt dependency in kapt or ksp scope, and for Groovy add jstachio-apt in compileOnly scope.

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.1.10 React SSR

React server-side rendering (SSR) allows you to pre-render React components to HTML before the page is sent to the user. This improves performance by ensuring the page appears before any Javascript has loaded (albeit in a non-responsive state) and makes it easier for search engines to index your pages.

This module is experimental and subject to change.

Micronaut’s support for React SSR has the following useful features:

  • Javascript runs using GraalJS, a high performance Javascript engine native to the JVM. Make sure to run your app on GraalVM or by compiling it to a native image to get full Javascript performance.

  • Compatible out of the box with both React and Preact, an alternative lighter weight implementation of the React concept.

  • Customize the Javascript used to invoke SSR to add features like head managers, or use the prepackaged default scripts to get going straight away.

  • The Javascript can be sandboxed, ensuring that your server environment is protected from possible supply chain attacks.

  • You can pass any @Introspectable Java objects to use as props for your page components. This is convenient for passing in things like the user profile info.

  • Logging from Javascript is sent to the Micronaut logs. console.log and related will go to the INFO level of the logger named js, console.error and Javascript exceptions will go to the ERROR level of the same.

To use React SSR you need to add two dependencies.

  1. Add the micronaut-views-react dependency.

  2. Add a dependency on org.graalvm.polyglot:js or org.graalvm.polyglot:js-community. The difference is to do with licensing and performance, with the js version being faster and free to use but not open source. Learn more about choosing an edition.

implementation("io.micronaut.views:micronaut-views-react")
<dependency>
    <groupId>io.micronaut.views</groupId>
    <artifactId>micronaut-views-react</artifactId>
</dependency>

Scaffold a Micronaut frontend/backend project

You can easily create a new ReactJS-based project from scratch using Micronaut Launch, the built-in IntelliJ Micronaut wizard, or the mn CLI tool:

$ mn create-app --features=views-react my-cool-project

This will create a complete project with webpack based bundling for client and server side rendering, a pre-configured Micronaut Views React app, and a sample component/page handler. Javascript package installation and bundling is handled by Gradle or Maven, depending on the value of the --build flag you pass to mn (we recommend Gradle as it will be much faster). The build scripts will even download NodeJS for you, so you don’t need anything other than a JDK to get started.

Configuration properties

The properties used can be customized by overriding the values of:

🔗
Table 1. Configuration Properties for ReactViewsRendererConfiguration
Property Type Description

micronaut.views.react.client-bundle-url

java.lang.String

the URL (relative or absolute) where the client Javascript bundle can be found. It will be appended to the generated HTML in a <script> tag. Defaults to "/static/client.js"

micronaut.views.react.server-bundle-path

java.lang.String

the path relative to micronaut.views.folder where the bundle used for server-side rendering can be found. Defaults to "classpath:views/ssr-components.mjs"

micronaut.views.react.render-script

java.lang.String

Either a file path (starting with "file:" or a resource in the classpath (starting with "classpath:") to a render script. Please see the user guide for more information on what this Javascript file should contain.

micronaut.views.react.sandbox

boolean

If true, GraalJS sandboxing is enabled. This helps protect you against supply chain attacks that might inject code into your server via hijacked React components. It requires a sufficiently new version of GraalJS. Defaults to OFF.

How it fits together

Props can be supplied in the form of an introspectable bean or a Map<String, Object>. Both forms will be serialized to JSON and sent to the client for hydration, as well as used to render the root component. The URL of the current page will be taken from the request and added to the props under the url key, which is useful when working with libraries like preact-router. If you use Map<String, Object> as your model type and use Micronaut Security, authenticated usernames and other security info will be added to your props automatically.

On the server side prop objects are exposed to Javascript directly, without being serialized to JSON first. Micronaut’s compile time reflection is used and this avoids some overhead as well as simplifying access to your props (see below). If your props bean returns a non-introspectable object from a property, it will be mapped in the normal way for GraalJS (meaning it will use runtime reflection and may require @HostAccess.Export on methods you wish to call).

By default, you will need React components that return the entire page, including the <html> tag. You’ll also need to prepare your Javascript (see below). Then just name your required page component in the @View annotation on a controller, for example @View("App") will render the <App/> component with your page props.

If your page components don’t render the whole page or you need better control over how the framework is invoked you can use render scripts (see below).

Accessing Java from Javascript

The usual GraalJS rules for accessing Java apply with a few differences:

  1. Your root prop object and any introspectable object reachable from it can be accessed using normal Javascript property syntax, for instance if you have an @Introspectable bean with a String getFoo() method then you can just access that property by writing props.foo instead of props.getFoo(), as would normally be required when accessing Java objects.

  2. Methods annotated with @Executable can be invoked from Javascript. Arguments and return values are mapped to/from Java in a natural manner.

  3. Your code can use Java.type("com.foo.bar.BazClass") style calls to get access to Java classes and then instantiate them or call static methods on them.

Note that props are read only. Attempting to set the value of a Java property on a props object will fail.

Sandbox

By default, Javascript executing server side runs with the same privilege level as the server itself. This is similar to the Node security model but exposes you to supply chain attacks. If a third party React component you depend on turns out to be malicious or simply buggy, it could allow an attacker to run code server side instead of only inside the browser sandbox.

Normally with React SSR you can’t do much about this, but with Micronaut Views React you can enable a server-side sandbox if you use GraalVM 24.1 or higher. This prevents Javascript from accessing any Java host objects that haven’t been specifically exposed into the sandbox. To use this set micronaut.views.react.sandbox to true in your application.properties.

In this mode:

  • The Java top level object that lets code access any class will be gone.

  • Methods in @Introspectable objects reachable from your prop objects that are marked as @Executable will be exposed into the sandbox regardless of sandbox settings. So be careful what methods you add to your props.

  • Any objects exposed via your root props that are not marked @Introspectable will be exposed via runtime reflection instead. In that case what’s available inside the sandbox will depend on the HostAccess policy, which can be customized by using the factory replacement mechanism (see the docs for Micronaut Core for details). By default anything not annotated with @HostAccess.Exposed will be invisible and uninvokable. Normally this is sufficient, but customizing the HostAccess can be useful if you want to expose third party code you don’t control into the sandbox.

3.1.10.1 Preparing your Javascript

An app that uses SSR needs the React components to be bundled twice, once for the client and once for the server. For the server you need to make a Javascript module bundle that imports and then re-exports the (page) components you will render, along with React and ReactServerDOM. The bundle must be compatible with GraalJS, which is not NodeJS and thus doesn’t support the same set of APIs. You will also need to create a client-side bundle as per usual, and change how you start up React.

This tutorial doesn’t take you through how to create a ReactJS project from scratch - please refer to the React documentation for that.

To start we will need a server.js file. It should be a part of your frontend project and can be named and placed wherever you like, as the server will only need the final compiled bundle. Your server.js should look like this:

src/main/js/server.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';

// Page components
import App from './components/App';

export { React, ReactDOMServer, App };

Add your page components as imports, and then also to the export line. We will now set up Webpack to turn this file into a bundle.

  1. Run npm i webpack node-polyfill-webpack-plugin text-encoding to install some extra packages that are needed.

  2. Create a config file called e.g. webpack.server.js like the following:

src/main/js/webpack.server.js
const path = require('path');
const webpack = require('webpack');

// This targets the browser even though we run it server-side, because that's closer to GraalJS.
module.exports = {
    entry: ['web-streams-polyfill/dist/polyfill', './server.js'],
    output: {
        path: path.resolve(__dirname, '../resources/views/'),
        filename: 'ssr-components.mjs',
        module: true,
        library: {
            type: 'module',
        },
        // GraalJS uses `globalThis` instead of `window` for the global object.
        globalObject: 'globalThis'
    },
    devtool: false,
    experiments: {
        outputModule: true
    },
    plugins: [
        new webpack.ProvidePlugin({
            // GraalJS doesn't support TextEncoder yet. It's easy to add and here's a polyfill in the meantime.
            TextEncoder: ['text-encoding', 'TextEncoder'],
            TextDecoder: ['text-encoding', 'TextDecoder'],
        }),
        new webpack.DefinePlugin({
            SERVER: true,
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react']
                    }
                }
            }
        ]
    }
};

This Webpack config does several things:

  • It polyfills APIs that lack a native implementation in the GraalJS engine.

  • It ensures the output is a native Javascript module.

  • It names the result ssr-components.mjs. You can use any name, but it’s looked for under this name in the views resource directory by default. All components must be in one server side bundle.

  • It makes the SERVER variable be statically true when the Javascript is being bundled for server-side rendering. This allows you to include/exclude code blocks at bundle optimization time.

You can use such a config by running npx webpack --mode production --config webpack.server.js. Add the --watch flag if you want the bundle to be recreated whenever an input file changes. Micronaut React SSR will notice if the bundle file has changed on disk and reload it (see Development).

Now create client.js. This will contain the Javascript that runs once the page is loaded, and which will "hydrate" the React app (reconnect the event handlers to the pre-existing DOM). It should look like this:

src/main/js/client.js
import React from 'react';
import {hydrateRoot} from 'react-dom/client';

const pageComponentName = Micronaut.rootComponent;

import(`./components/${pageComponentName}.js`).then(module => {
    const PageComponent = module[pageComponentName]
    hydrateRoot(document, <PageComponent {...Micronaut.rootProps}/>)
})

Depending on how you configure minification, you may also need to import your page components here. This small snippet of code reads the Micronaut object which is generated by the Micronaut React SSR renderer just before your client.js code is loaded. It contains the component named in your @View("MyPageComponent") annotation, which is then loaded assuming it is in a Javascript module of the same name. The props that will be passed to that page component as generated from the object you return from your controller method. If you wish you can wrap <PageComponent/> here with any contexts you need.

And now for the webpack.client.js config:

src/main/js/webpack.client.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: ['./client.js'],
    devtool: false,
    output: {
        path: path.resolve(__dirname, '../resources/views/static'),
        filename: 'client.js',
    },
    plugins: [
        new webpack.DefinePlugin({
            SERVER: false,
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react']
                    }
                }
            }
        ]
    }
};

It tells Webpack to generate a series of JS files that are then placed in the src/main/resources/static directory.

Run Webpack to generate the needed Javascript files for both client and server.

3.1.10.2 Setting serving properties

React SSR needs some Micronaut application properties to be set.

micronaut.views.react.server-bundle-path=classpath:views/ssr-components.mjs
micronaut.router.static-resources.js.mapping=/static/**
micronaut.router.static-resources.js.paths=classpath:static
micronaut.executors.blocking.virtual=false
micronaut:
  # Point to the server-side JS. This value is the default.
  views:
    react:
      server-bundle-path: "classpath:views/ssr-components.mjs"

  router:
    static-resources:
      js:
        mapping: "/static/**"
        paths: "classpath:static"

  # A temporary workaround for a GraalJS limitation.
  executors:
    blocking:
        virtual: false
[micronaut]
  [micronaut.views]
    [micronaut.views.react]
      server-bundle-path="classpath:views/ssr-components.mjs"
  [micronaut.router]
    [micronaut.router.static-resources]
      [micronaut.router.static-resources.js]
        mapping="/static/**"
        paths="classpath:static"
  [micronaut.executors]
    [micronaut.executors.blocking]
      virtual=false
micronaut {
  views {
    react {
      serverBundlePath = "classpath:views/ssr-components.mjs"
    }
  }
  router {
    staticResources {
      js {
        mapping = "/static/**"
        paths = "classpath:static"
      }
    }
  }
  executors {
    blocking {
      virtual = false
    }
  }
}
{
  micronaut {
    views {
      react {
        server-bundle-path = "classpath:views/ssr-components.mjs"
      }
    }
    router {
      static-resources {
        js {
          mapping = "/static/**"
          paths = "classpath:static"
        }
      }
    }
    executors {
      blocking {
        virtual = false
      }
    }
  }
}
{
  "micronaut": {
    "views": {
      "react": {
        "server-bundle-path": "classpath:views/ssr-components.mjs"
      }
    },
    "router": {
      "static-resources": {
        "js": {
          "mapping": "/static/**",
          "paths": "classpath:static"
        }
      }
    },
    "executors": {
      "blocking": {
        "virtual": false
      }
    }
  }
}

This sets up static file serving so your client JS will be served by your Micronaut app. This isn’t mandatory: you can serve your client JS from anywhere, but you would need to set micronaut.views.react.client-bundle-url in that case to where the client root bundle can be found.

Watch out for the last property that disables virtual threads. If you skip this you will get an error the first time a view is rendered. Future releases of GraalJS will remove the need to disable virtual threads in Micronaut.

Development

During development you want the fastest iteration speed possible. Firstly turn off response caching so hot reload works with npx webpack --watch. Micronaut Views React will automatically notice the file changed on disk and reload it.

micronaut.server.netty.responses.file.cache-seconds=0
micronaut:
  # For development purposes only.
  server:
    netty:
      responses:
        file:
          cache-seconds: 0
[micronaut]
  [micronaut.server]
    [micronaut.server.netty]
      [micronaut.server.netty.responses]
        [micronaut.server.netty.responses.file]
          cache-seconds=0
micronaut {
  server {
    netty {
      responses {
        file {
          cacheSeconds = 0
        }
      }
    }
  }
}
{
  micronaut {
    server {
      netty {
        responses {
          file {
            cache-seconds = 0
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "netty": {
        "responses": {
          "file": {
            "cache-seconds": 0
          }
        }
      }
    }
  }
}

If using Maven turn off Micronaut’s automatic restart features so that changes to the compiled bundle JS don’t cause the whole server to reboot:

<plugin>
    <groupId>io.micronaut.maven</groupId>
    <artifactId>micronaut-maven-plugin</artifactId>
    <version>...</version>
    <configuration>
        <watches>
            <watch>
                <directory>src/main/resources</directory>
                <excludes>
                    <exclude>**/*.js</exclude>
                    <exclude>**/*.mjs</exclude>
                </excludes>
            </watch>
        </watches>
    </configuration>
</plugin>

3.1.10.3 Integrating with Preact

The Preact library is a smaller and lighter weight implementation of React, with a few nice enhancements as well. Like React it also supports server side rendering and can be used with Micronaut React SSR. It requires some small changes to how you prepare your Javascript. Please read and understand how to prepare your JS for regular React first, as this section only covers the differences.

Your server.js should look like this:

src/main/js/server.js
import {renderToString} from 'preact-render-to-string';
import * as preact from 'preact';

// Page components
import App from './components/App';

export { preact, renderToString, App };

Notice the differences: we’re re-exporting the h symbol from Preact (which it uses instead of React.createComponent) and renderToString from the separate preact-render-to-string module. Otherwise the script is the same: we have to export each page component.

Your client.js should look like this:

src/main/js/client.js
import {h, render} from 'preact'

const pageComponentName = Micronaut.rootComponent;

import(`./components/${pageComponentName}.js`).then(module => {
    const PageComponent = module[pageComponentName]
    render(h(PageComponent, Micronaut.rootProps, null), document)
})

Finally, you need to tell Micronaut Views React to use a different render script (see below). Set the micronaut.views.react.render-script application property to be classpath:/io/micronaut/views/react/preact.js.

That’s it. If you want to use existing React components then you will also need to set up aliases in your webpack.{client,server}.js files like this:

module.exports = {
    // ... existing values
    resolve: {
        alias: {
            "react": "preact/compat",
            "react-dom/test-utils": "preact/test-utils",
            "react-dom": "preact/compat",     // Must be below test-utils
            "react/jsx-runtime": "preact/jsx-runtime"
        },
    }
}

3.1.10.4 Render scripts

The code that kicks off the SSR process using your React libraries API is called a render script. Micronaut Views React ships with two pre-packaged render scripts, one for ReactJS and one for Preact, but you are also able to supply your own. This lets you take complete control over the server-side Javascript. To use a custom script, place it somewhere on your classpath or file system and then set the micronaut.views.react.render-script property to its path, prefixed with either classpath: or file: depending on where it should be found.

A render script should be an ESM module that exports a single function called ssr that takes four arguments:

  1. A function object for the page component to render.

  2. An object containing the root props.

  3. A callback object that contains APIs used to communicate with Micronaut.

  4. A string that receives the URL of the bundle that the browser should load. This is specified by the micronaut.views.react.clientBundleURL application property.

The default render script looks like this:

classpath:/io/micronaut/views/react/react.js
export async function ssr(component, props, callback, clientBundleURL) {
    globalThis.Micronaut = {};
    const url = callback.url();
    if (url)
        props = {...props, "url": url};
    const element = React.createElement(component, props, null);

    // Data to be passed to the browser after the main HTML has finished loading.
    const boot = {
        rootProps: props,
        rootComponent: component.name,
    };

    // The Micronaut object defined here is not the same as the Micronaut object defined server side.
    const bootstrapScriptContent = `var Micronaut = ${JSON.stringify(boot)};`;
    const stream = await ReactDOMServer.renderToReadableStream(element, {
        bootstrapScriptContent: bootstrapScriptContent,
        bootstrapScripts: [clientBundleURL]
    });

    // This ugliness is because renderToPipeableStream (what we should really use) is only in the node build
    // of react-dom/server, but we use the browser build. Trying to use the node build causes various errors
    // and problems that I don't yet understand, something to do with module formats.
    const reader = stream.getReader();
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        callback.write(value);
    }
}

The default render script for Preact looks like this:

classpath:/io/micronaut/views/react/preact.js
export function ssr(component, props, callback, clientBundleURL) {
    globalThis.Micronaut = {};
    const url = callback.url();
    if (url)
        props = {...props, "url": url};
    const html = renderToString(preact.h(component, props, null))
    callback.write(html)
    const boot = {
        rootProps: props,
        rootComponent: component.name,
    };

    // The Micronaut object defined here is not the same as the Micronaut object defined server side.
    callback.write(`<script type="text/javascript">var Micronaut = ${JSON.stringify(boot)};</script>`)
    callback.write(`<script type="text/javascript" src="${clientBundleURL}" async="true">`)
}

A more sophisticated render script might support the use of head managers (see below), do multiple renders, expose other APIs and so on.

A render script is evaluated after your server side bundle, and has access to any symbols your server script exported. If you wish to access a JS module you should therefore include it in your server.js that gets fed to Webpack or similar bundler, and then re-export it like this:

import * as mymod from 'mymod';
export { mymod };

The callback object has a few different APIs you can use:

  1. write(string): Writes the given string to the network response.

  2. write(bytes): Writes the given array of bytes to the network response.

  3. url(): Returns either null or a string containing the URL of the page being served. Useful for sending to page routers.

3.1.10.4.1 Using head managers

Head managers are libraries that let you build up the contents of your <head> block as your <body> renders. One use of custom render scripts is to integrate a head manager with your code. Here’s an example of a simple render script that usees the React Helmet library in this way. Remember to export Helmet from your server-side bundle.

export async function ssr(component, props, callback, config) {
    // Create the vdom.
    const element = React.createElement(component, props, null);
    // Render the given component, expecting it to fill a <div id="content"></div> in the <body> tag.
    const body = ReactDOMServer.renderToString(element)
    // Get the data that should populate the <head> from the Helmet library.
    const helmet = Helmet.renderStatic();
    // Data to be passed to the browser after the main HTML has finished loading.
    const boot = {
        rootProps: props,
        rootComponent: component.name,
    };

    // Assemble the HTML.
    const html = `
    <!doctype html>
    <html ${helmet.htmlAttributes.toString()}>
        <head>
            ${helmet.title.toString()}
            ${helmet.meta.toString()}
            ${helmet.link.toString()}
        </head>
        <body ${helmet.bodyAttributes.toString()}>
            <div id="content">
                ${body}
            </div>

            <script>var Micronaut = ${JSON.stringify(boot)};</script>
            <script type="text/javascript" src="${config.getClientBundleURL()}" async="true"></script>
        </body>
    </html>`;

    // Send it back.
    callback.write(html);
}

3.1.10.5 Known limitations

Micronaut React SSR has the following known issues and limitations:

  • There is no built-in support for server side fetching.

  • The rendering isn’t streamed to the user.

  • <Suspense> is not supported.

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 CSRF Token Hidden Field

If you use the Micronaut Security CSRF module, and a CSRF token is resolved, the generated form automatically contains a hidden input with the CSRF token as the value.

3.3.5 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 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

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

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

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

Sets whether CSP is enabled. Default value (false).

micronaut.views.csp.policy-directives

java.lang.String

Sets the policy directives.

micronaut.views.csp.report-only

boolean

If true, the Content-Security-Policy-Report-Only header will be sent instead of Content-Security-Policy. Default value (false).

micronaut.views.csp.random-engine

java.util.Random

The Random data engine used to generate nonce values. Ignored if forceSecureRandom is set to true.

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

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:

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';

The nonce value can be accessed on the server as a request attribute named cspNonce. This is the value to use in the nonce attribute on script and related tags. For example (adapt as appropriate for your template language):

<script type="text/javascript" src="/path/to/script.js" nonce="${cspNonce}" />

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:

🔗
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

Model key name. Default value ("security").

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

java.lang.String

Nested security map key for the user’s name property. Default value ("name").

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

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:

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.

CSRF Token View Model Processor

If you use the Micronaut Security CSRF module, there is also a view model processor for a model of type Map<String, Object>. If a CSRF Token can be resolved, CSRFViewModelProcessor adds it to the model.

The following properties allow you to customize the injection:

🔗
Table 2. Configuration Properties for CsrfViewModelProcessorConfigurationProperties
Property Type Description

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

java.lang.String

Model key for CSRF Token. Default value ("csrfToken").

micronaut.security.csrf.views-model-decorator.enabled

boolean

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

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.

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: