Java / Gradle

Hotwire Turbo Chat with Micronaut Views

This guide shows how to build a Micronaut Framework chat application such as the Rails application demonstrated in the Hotwire announcement screencast.

Sergio del Amo
On this guide
In this section

What you will need

To complete this guide, you will need the following:

Getting started

This guide shows a chat application such as the Rails application demonstrated in the Hotwire announcement screencast.

What is Hotwire?

Hotwire is an umbrella for trio frameworks that implement the HTML-over-the-wire approach to building modern web applications. At its heart is Turbo, which gives you techniques for bringing the speed of a single-page application without writing a lick of JavaScript.

This guide primarily focuses on how Turbo works within a Micronaut application.

Screencast

The following screencast shows this guide in action:

Download Solution

Download and unzip the source of the guide. You will find two folders:

  • initial. It contains a Micronaut application without any Turbo Integration.

  • complete. The resulting Micronaut application if you follow the instructions in the next sections and apply these changes to the initial application.

Initial application introduction

The initial application uses two models, Room and Message. One Room has many Messages.

The initial application contains a basic editing interface for Chat Rooms. It uses Micronaut Views Thymeleaf to render server-side HTML

Moreover, the application leverages Thymeleaf Fragments to encapsulate the rendering of parts of the screen.

For Messages, we will have just two actions. create to render the form to create a message and save to handle the form submission.

It gives us a foundation flow for an admittedly cumbersome chat application, which we can then use to level up with Hotwire techniques one at a time.

Appendix A: Initial application describes the initial application if you want to learn more.

Install Turbo

Install Turbo in compiled form by referencing the Turbo distributable script directly in the <head> of your application.

Modify the initial application, replace src/main/resources/views/layout.html.

<!DOCTYPE html>
...
    <head>
    ...
    <script type="module">
        import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
    </script>
...
    </head>
...

Turbo Frames

So let’s introduce our first Turbo feature, Frames.

Turbo Frames decompose pages into independent contexts, which can be lazy-loaded and scope interaction.

So when you follow a link or submit a form, only the content of the Frame changes rather than the entire page.

This allows you to keep the state of the rest of the page from changing, making the app feel more responsive.

Highlight Frames

To see how the Frames work easily, we’ll call them out with a blue border.

complete/java/src/main/resources/assets/stylesheets/application.css
turbo-frame {
    display: block;
    border: 1px solid blue;
}

Turbo Frame Show View

Now let’s wrap the Room name and the ability to edit it inside a Frame.

Replace this:

initial/java/src/main/resources/views/rooms/show.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <p th:replace="rooms/_room :: room(${room})"></p>
        <p>
            <a th:href="@{|/rooms/${room.id}/edit|}" th:text="#{action.edit}"></a> |
            <a href="/rooms" th:text="#{action.back}"></a>
        </p>
        <div id="messages">
            <div th:each="message : ${room.messages}">
                <p th:replace="messages/_message :: message(${message})"></p>
            </div>
        </div>
        <a th:href="@{|/rooms/${room.id}/messages/create|}" th:text="#{message.new}"></a>
    </main>
</body>
</html>

with:

src/main/resources/views/rooms/show.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <turbo-frame id="room">
            <p th:replace="rooms/_room :: room(${room})"></p>
            <p>
                <a th:href="@{|/rooms/${room.id}/edit|}" th:text="#{action.edit}"></a> |
                <a href="/rooms" th:text="#{action.back}"></a>
           </p>
        </turbo-frame>
        <div id="messages">
            <div th:each="message : ${room.messages}">
                <p th:replace="messages/_message :: message(${message})"></p>
            </div>
        </div>
        <a th:href="@{|/rooms/${room.id}/messages/create|}" th:text="#{message.new}"></a>
    </main>
</body>
</html>

Please, note the usage of <turbo-frame id=" room"> in the previous code snippet.

The Turbo Frame tag goes around the initial display, including the edit link and the part of the edit page we want to appear within the frame.

Turbo Frame Edit View

Replace this:

initial/java/src/main/resources/views/rooms/edit.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <h1 th:text="#{room.edit}"></h1>
<p th:replace="rooms/_edit :: edit(${room})"></p>
        <a th:href="@{|/rooms/${room.id}|}" th:text="#{action.show}"></a> |
        <a href="/rooms" th:text="#{action.back}"></a>
    </main>
</body>
</html>

with this:

src/main/resources/views/rooms/edit.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <h1 th:text="#{room.edit}"></h1>
        <turbo-frame id="room">
            <p th:replace="rooms/_edit :: edit(${room})"></p>
        </turbo-frame>
        <a th:href="@{|/rooms/${room.id}|}" th:text="#{action.show}"></a> |
        <a href="/rooms" th:text="#{action.back}"></a>
    </main>
</body>
</html>

We see our frame wrapped in blue.

And when clicking the Edit link, the form from the Edit screen is presented.

And upon submission, it’s replaced again with just a display.

If we go straight to the full page editing screen, we can see it has both a header and navigation links, parts we were omitting from the frame.

Underscore Top

Note that if we try to click a link within the frame that goes somewhere without a matching Frame, nothing happens.

We can solve this by adding a Data Turbo Frame attribute that points to _top to break out of the frame, just like traditional HTML frames.

Replace:

src/main/resources/views/rooms/show.html
....
<body>
    <main>
        ...
        <turbo-frame id="room">
            ...
                <a href="/rooms" th:text="#{action.back}"></a>
           </p>
        </turbo-frame>
....

with:

src/main/resources/views/rooms/show.html
....
<body>
    <main>
        ...
        <turbo-frame id="room">
            ...
               <a data-turbo-frame="_top" href="/rooms" th:text="#{action.back}"></a>
           </p>
        </turbo-frame>
....

Now the backlink works, and the frame scopes the edit display loop.

Lazy Loading Frames

Then, let’s add the New Message link into an inline but lazy-loaded Turbo Frame tag that also, just for starters, acts on the whole page.

This frame will be loaded right after the page displays, hitting the New Message Controller action we made earlier.

Replace:

src/main/resources/views/rooms/show.html
...
...
        <a href="/messages/create" th:text="#{message.new}"></a>
    </main>
</body>
</html>

with:

src/main/resources/views/rooms/show.html
....
        <turbo-frame id="new_message"
                     th:src="@{|/rooms/${room.id}/messages/create|}"
                     target="_top"></turbo-frame>
    </main>
</body>
</html>

Plug out the Frame

Like with edit, we wrap the relevant segment in a Frame tag with a matching ID, which is how Turbo knows how to plug out the right frame.

Replace:

initial/java/src/main/resources/views/messages/create.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
    <head>
        <script></script>
    </head>
<body>
    <main>
    <h1 th:text="#{message.new}"></h1>
<form th:replace="messages/_create :: create(${room})"></form>
    <a th:href="@{|/rooms/${room.id}|}" th:text="#{action.back}"></a>
    </main>
</body>
</html>

with:

complete/java/src/main/resources/views/messages/create.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
    <h1 th:text="#{message.new}"></h1>
    <turbo-frame id="new_message" target="_top">
<form th:replace="messages/_create :: create(${room})"></form>
    </turbo-frame>
    <a th:href="@{|/rooms/${room.id}|}" th:text="#{action.back}"></a>
    </main>
</body>
</html>

You can now see two requests when we load the room: one for the page, and one for the lazy-loader frame.

Let’s try to add a message.

It works!

But this only demonstrates that the frame was lazy-loaded.

Right now, we’re resetting the whole page upon submission of the New Message form.

Whereas with the Room Name Frame, you can edit and submit without changing the rest of the page state, a real independent context.

You can see how the Frame replacement happens by inspecting the response to edit.

Turbo will plug out just the matching frame from the server response. As you can see here, the header and links are ignored.

TurboFrameView Annotation

In a Micronaut application, we can optimize the response by using the @TurboFrameView annotation only to render the layout Turbo uses when parsing the response. A request coming from a frame includes the HTTP header Turbo-Frame. Annotate the RoomsControllerEdit::edit method with @TurboFrameView("/rooms/_edit").

complete/java/src/main/java/example/micronaut/controllers/RoomsControllerEdit.java

The above controller returns the following HTML for a request without the HTTP header Turbo-Frame.

<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link rel="stylesheet" media="all" href="/assets/stylesheets/application.css" />
        <link rel="stylesheet" media="all" href="/assets/stylesheets/scaffolds.css" />
        <script type="module">
            import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
        </script>
    </head>
    <body>
        <main>
            <h1>Editing Room</h1>
            <turbo-frame id="room">
                <form action="/rooms/update"
                      accept-charset="UTF-8"
                      method="post">
                     <input type="hidden" value="1" name="id">
                     <div class="field">
                         <label for="room_name">Name</label>
                         <input type="text" value="Micronaut Questions" name="name" id="room_name" />
                     </div>
                     <div class="actions">
                         <input type="submit" name="commit" value="Update Room"/>
                     </div>
                </form>
            </turbo-frame>
            <a href="/rooms/1">Show</a> |
            <a href="/rooms">Back</a>
        </main>
    </body>
</html>

For a request including an HTTP header Turbo-Frame with value rooms, the above controller returns the following HTML.

<turbo-frame id="room">
    <form action="/rooms/update"
          accept-charset="UTF-8"
          method="post">
        <input type="hidden" value="1" name="id">
        <div class="field">
            <label for="room_name">Name</label>
            <input type="text" value="Micronaut Questions" name="name" id="room_name" />
        </div>
        <div class="actions">
            <input type="submit" name="commit" value="Update Room"/>
        </div>
    </form>
</turbo-frame>

Turbo Streams

Turbo Streams deliver page changes over WebSocket or in response to form submissions using just HTML and a set of CRUD-like action tags.

Turbo Streams let you append or prepend to replace and remove any target DOM element from the existing page.

They’re strictly limited to DOM changes, though. No direct JavaScript invocation.

If you need more than a DOM change, connect a Stimulus controller.

We will add a Turbo stream response to the message creation action such that we can add the new message to the Room page without replacing the whole page.

This template invokes the append action with the DOM ID of the target container and either a full set of partial rendering options or just a record we wish to render which conforms to the naming conventions for matching to a partial.

complete/java/src/main/java/example/micronaut/controllers/MessagesControllerSave.java

Now we can add Messages to the page without resetting it completely.

Stimulus Controller

The Edit Name form can stay open while we’re doing this because new Messages are added directly to the Messages div. The Turbo Stream HTML is rendered directly in response to the form submission, and Turbo knows from the MIME type to process it automatically. But notice the input field isn’t cleared. We can fix that by adding a Stimulus controller.


Stimulus is a modest JavaScript framework for the HTML you already have. _

complete/java/src/main/resources/assets/javascripts/controllers/reset_form_controller.mjs
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"

export default class extends Controller {
    reset() {
        this.element.reset()
    }
}

and register it:

complete/java/src/main/resources/views/layout.html
-->
    <script type="module">
        import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
        import {Application} from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
        import ResetFormController from "/assets/javascripts/controllers/reset_form_controller.mjs"
        window.Stimulus = Application.start()
        Stimulus.register("reset-form", ResetFormController)
    </script>
<!--

The Stimulus controller we’re going to add will be a dead-simple way to reset the form after creating a new Message.

It has just one method, Reset, which we will call when Turbo is done submitting the form via Fetch.

Add the data-controller and data-action attributes to the form:

complete/java/src/main/resources/views/messages/_create.html
<form th:fragment="create(room)"
      th:action="@{|/rooms/${room.id}/messages|}"
      data-controller="reset-form"
      data-action="turbo:submit-end->reset-form#reset"
      accept-charset="UTF-8"
      method="post">
    <div class="field">
        <input type="text" name="content" id="message_content"/>
        <input type="submit" name="commit" th:value="#{message.send}"/>
    </div>
</form>

The form is reset, and the Message is added dynamically.

Turbo Streams via Web Sockets

But how interesting is a chat app where you’re just talking to yourself? Let’s start a conversation with another window. You’ll see that new Messages are only added live to the originator’s window. On the other side, we have to reload to see what’s been said.

Let’s fix that.

Events

When the message is saved, raise an event:

complete/java/src/main/java/example/micronaut/services/DefaultMessageService.java

WebSocket Server

Create a WebSocket Server, which publishes a Turbo Stream when a message event is received.

complete/java/src/main/java/example/micronaut/ChatServerWebSocket.java

Establish a WebSocket connection to the WebSocket server identified by the Room we’re in.

complete/java/src/main/resources/views/rooms/show.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script type="module">
        const wsUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/chat/[[${room.id}]]";
        const socket = new WebSocket(wsUrl);
        socket.addEventListener('open', function (event) {
            console.log(event);
        });
        import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
        Turbo.session.connectStreamSource(socket);
    </script>
</head>
<body>
    <main>
        <turbo-frame id="room">
        <p th:replace="rooms/_room :: room(${room})"></p>
        <p>
            <a th:href="@{|/rooms/${room.id}/edit|}" th:text="#{action.edit}"></a> |
            <a data-turbo-frame="_top" href="/rooms" th:text="#{action.back}"></a>
        </p>
        </turbo-frame>
        <div id="messages">
            <div th:each="message : ${room.messages}">
                <p th:replace="messages/_message :: message(${message})"></p>
            </div>
        </div>
        <turbo-frame id="new_message"
                     th:src="@{|/rooms/${room.id}/messages/create|}"
                     target="_top"></turbo-frame>
    </main>
</body>
</html>

Now we can add a new message and see it appear in both windows.

Next

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire.

We get to keep all our template rendering on the server, which means writing more of our applications in our favorite programming languages.

Appendix A: Initial application

The following sections introduce you to the initial application.

Data Source configuration

Define the datasource in src/main/resources/application.properties.

initial/java/src/main/resources/application.properties
datasources.default.dialect=MYSQL
datasources.default.driver-class-name=${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}
Note
This way of defining the datasource properties enables us to externalize the configuration, for example for production environment, and also provide a default value for development. If the environment variables are not defined, the Micronaut framework will use the default values.

Database Schema

Database Migration with Flyway

We need a way to create the database schema. For that, we use Micronaut integration with Flyway.

Flyway automates schema changes, significantly simplifying schema management tasks, such as migrating, rolling back, and reproducing in multiple environments.

Add the following snippet to include the necessary dependencies:

build.gradle
implementation("io.micronaut.flyway:micronaut-flyway")

We will enable Flyway in the Micronaut configuration file and configure it to perform migrations on one of the defined data sources.

Since Flyway 8.2.0, the Flyway distribution does not contain the MySQL driver.

Add the following dependency:

build.gradle
implementation("org.flywaydb:flyway-mysql:@flyway-mysqlVersion@")
initial/java/src/main/resources/application.properties
Note
Configuring multiple data sources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each data source. Review the Micronaut Flyway documentation for additional details.

Flyway migration will be automatically triggered before your Micronaut application starts. Flyway will read migration commands in the resources/db/migration/ directory, execute them if necessary, and verify that the configured data source is consistent with them.

Create the following migration files with the database schema creation:

initial/java/src/main/resources/db/migration/V1__schema.sql
DROP TABLE IF EXISTS room;
DROP TABLE IF EXISTS message;

CREATE TABLE room (
    id   BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
    name  VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE message (
    id   BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
    content  VARCHAR(255) NOT NULL,
    room_id BIGINT,
    date_created datetime NULL,
    INDEX r_id (room_id),
    FOREIGN KEY (room_id)
        REFERENCES room(id)
        ON DELETE CASCADE
);

Entities

The application contains two entities with a one-to-many relationship.

initial/java/src/main/java/example/micronaut/entities/Room.java
initial/java/src/main/java/example/micronaut/entities/Message.java

Models

The application includes a POJO to map the form submission when the user submits a message to a room.

initial/java/src/main/java/example/micronaut/models/MessageForm.java

The application includes a POJO which represents a room’s message.

initial/java/src/main/java/example/micronaut/models/RoomMessage.java

Repositories

The application includes a repository per entity.

initial/java/src/main/java/example/micronaut/repositories/MessageRepository.java
initial/java/src/main/java/example/micronaut/repositories/RoomRepository.java

Services

The application contains a service that publishes an event when a message is saved.

initial/java/src/main/java/example/micronaut/services/MessageService.java
initial/java/src/main/java/example/micronaut/services/DefaultMessageService.java

Static Resources

Update application.properties to add static resource configuration:

initial/java/src/main/resources/application.properties

Views

To use the Thymeleaf Java template engine to render views in a Micronaut application, add the following dependency on your classpath.

build.gradle
implementation("io.micronaut.views:micronaut-views-thymeleaf")

The initial application uses Thymeleaf Fragments to organize the views.

It uses a root layout:

initial/java/src/main/resources/views/layout.html
<!DOCTYPE html>
<html th:fragment="layout (script, content)" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Chat</title>
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <link rel="stylesheet" media="all" href="/assets/stylesheets/application.css" />
        <link rel="stylesheet" media="all" href="/assets/stylesheets/scaffolds.css" />
        <script th:replace="${script}"></script>
    </head>
    <body>
    <div th:replace="${content}"></div>
    </body>
</html>

Properties

Create a default messages.properties file:

initial/java/src/main/resources/i18n/messages.properties
chat=Chat
message.send=Send
message.new=New Message
room.name=Name
room.list=Rooms
room.new=New Room
room.edit=Editing Room
room.update=Update Room
room.create=Create Room
action.back=Back
action.show=Show
action.edit=Edit
action.destroy=Destroy

Create a messages_es.properties file for the Spanish locale:

initial/java/src/main/resources/i18n/messages_es.properties
chat=Chat
message.send=Enviar
message.new=Crear Mensaje
room.name=Nombre
room.list=Salas
room.new=Nueva Sala
room.edit=Editar Sala
room.update=Actualizar Sala
room.create=Crear Sala
action.back=Volver
action.show=Mostrar
action.edit=Editar
action.destroy=Eliminar

Message Source

Create a MessageSource that uses the previous properties files:

initial/java/src/main/java/example/micronaut/i18n/MessageSourceFactory.java

Controllers

The apex url is redirected to /rooms.

initial/java/src/main/java/example/micronaut/controllers/HomeController.java

We have an abstract class to simplify redirection:

initial/java/src/main/java/example/micronaut/controllers/ApplicationController.java
package example.micronaut.controllers;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.uri.UriBuilder;

public abstract class ApplicationController {
    @NonNull
    protected HttpResponse<?> redirectTo(@NonNull CharSequence uri,
                                         @NonNull Long id) {
        return HttpResponse.seeOther(UriBuilder.of(uri)
                .path("" + id)
                .build());
    }
}

Create CRUD controllers for Room.

Rooms Index Controller

Create a controller that displays a list of rooms.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerIndex.java
Rooms Index Views

The controller uses Thymeleaf to render server-side HTML.

initial/java/src/main/resources/views/rooms/index.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <h1 th:text="#{room.list}"></h1>
<table th:replace="rooms/_table :: table(${rooms})"></table>
        <br />
        <a href="/rooms/create"
           th:text="#{room.new}"></a>
    </main>
</body>
</html>
initial/java/src/main/resources/views/rooms/_table.html
<table th:fragment="table(rooms)">
    <thead>
    <tr>
        <th th:text="#{room.name}"></th>
        <th colspan="3"></th>
    </tr>
    </thead>
    <tbody>
<tr th:each="room : ${rooms}" th:include="rooms/_tr :: tr(${room})"></tr>
    </tbody>
</table>
initial/java/src/main/resources/views/rooms/_tr.html
<tr th:fragment="tr(room)">
    <td th:text="${room.name}"></td>
    <td><a th:href="@{|/rooms/${room.id}|}" th:text="#{action.show}"></a></td>
    <td><a th:href="@{|/rooms/${room.id}/edit|}" th:text="#{action.edit}"></a></td>
    <td>
        <form th:action="@{|/rooms/${room.id}/delete|}" method="post" onsubmit="return confirm('Are you sure?');">
            <input type="hidden" name="id" th:value="${room.id}" />
            <input type="submit" th:value="#{action.destroy}"/>
        </form>
    </td>
</tr>

Rooms Show Controller

Create a controller that displays a room.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerShow.java
Rooms Show Views

Add Thymeleaf templates to render server-side HTML.

initial/java/src/main/resources/views/rooms/show.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <p th:replace="rooms/_room :: room(${room})"></p>
        <p>
            <a th:href="@{|/rooms/${room.id}/edit|}" th:text="#{action.edit}"></a> |
            <a href="/rooms" th:text="#{action.back}"></a>
        </p>
        <div id="messages">
            <div th:each="message : ${room.messages}">
                <p th:replace="messages/_message :: message(${message})"></p>
            </div>
        </div>
        <a th:href="@{|/rooms/${room.id}/messages/create|}" th:text="#{message.new}"></a>
    </main>
</body>
</html>
initial/java/src/main/resources/views/rooms/_room.html
<p th:fragment="room(room)" th:id="@{|room_${room.id}|}">
    <strong th:text="#{room.name}">:</strong>
    [[${room.name}]]
</p>

Rooms Create Controller

Create a controller that displays a form to create a room.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerCreate.java
Rooms Create Views

Add Thymeleaf templates to render a form.

initial/java/src/main/resources/views/rooms/create.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <h1 th:text="#{room.new}"></h1>
<form th:replace="rooms/_create :: create()"></form>
        <a href="/rooms" th:text="#{action.back}"></a>
    </main>
</body>
</html>
initial/java/src/main/resources/views/rooms/_create.html
<form th:fragment="create()"
      action="/rooms"
      accept-charset="UTF-8" method="post">
    <div class="field">
        <label for="room_name" th:text="#{room.name}"></label>
        <input type="text" name="name" id="room_name" />
    </div>
    <div class="actions">
        <input type="submit" name="commit" th:value="#{room.create}" />
    </div>
</form>

Rooms Save Controller

Create a controller that handles the room creation form submission.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerSave.java

Rooms Edit Controller

Create a controller that shows a form to edit a room.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerEdit.java
Rooms Edit Views

Add Thymeleaf templates to render an edit form.

initial/java/src/main/resources/views/rooms/edit.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <script></script>
</head>
<body>
    <main>
        <h1 th:text="#{room.edit}"></h1>
<p th:replace="rooms/_edit :: edit(${room})"></p>
        <a th:href="@{|/rooms/${room.id}|}" th:text="#{action.show}"></a> |
        <a href="/rooms" th:text="#{action.back}"></a>
    </main>
</body>
</html>
initial/java/src/main/resources/views/rooms/_edit.html
<form th:fragment="edit(room)"
      action="/rooms/update"
      accept-charset="UTF-8"
      method="post">
    <input type="hidden" th:value="${room.id}" name="id" />
    <div class="field">
        <label for="room_name" th:text="#{room.name}"></label>
        <input type="text" th:value="${room.name}" name="name" id="room_name" />
    </div>
    <div class="actions">
        <input type="submit" name="commit" th:value="#{room.update}"/>
    </div>
</form>

Rooms Update Controller

Create a controller that handles the room update form submission.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerUpdate.java

Rooms Delete Controller

Create a controller that handles the room deletion form submission.

initial/java/src/main/java/example/micronaut/controllers/RoomsControllerDelete.java

Message Create Controller

Create a controller to display a form to create a message within a room.

initial/java/src/main/java/example/micronaut/controllers/MessagesControllerCreate.java
Message Create Views

The controller uses Thymeleaf views.

initial/java/src/main/resources/views/messages/_create.html
<form th:fragment="create(room)"
      th:action="@{|/rooms/${room.id}/messages|}"
      accept-charset="UTF-8"
      method="post">
    <div class="field">
        <input type="text" name="content" id="message_content"/>
        <input type="submit" name="commit" th:value="#{message.send}"/>
    </div>
</form>
initial/java/src/main/resources/views/messages/_message.html
<p th:fragment="message(message)" th:id="@{|message_${message.id}|}">
    [[${message.formattedDateCreated()}]]: [[${message.content}]]
</p>
initial/java/src/main/resources/views/messages/create.html
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
    <head>
        <script></script>
    </head>
<body>
    <main>
    <h1 th:text="#{message.new}"></h1>
<form th:replace="messages/_create :: create(${room})"></form>
    <a th:href="@{|/rooms/${room.id}|}" th:text="#{action.back}"></a>
    </main>
</body>
</html>

Message Save Controller

Create a controller that handles the message creation form submission.

initial/java/src/main/java/example/micronaut/controllers/MessagesControllerSave.java

Test Resources

When the application is started locally, either under test or while running locally, resolution of the datasource URL is detected and the Test Resources service will start a local MySQL docker container, and inject the properties required to use this as the datasource.

For more information, see the JDBC section or R2DBC section of the Test Resources documentation.

Running the application

To run the application, use the ./gradlew run command, which starts the application on port 8080.

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).