Thymeleaf
Thymeleaf er en template engine, som vi bruger til at generere HTML-sider fra vores Java-applikation.
En Thymeleaf-template er almindelig HTML med ekstra attributter, som begynder med th:.
Det betyder, at en HTML-side både kan ligne almindelig HTML og indeholde dynamisk indhold.
For at bruge Thymeleaf skal du have følgende i pom.xml (samme version som din Javalin)
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin-rendering-thymeleaf</artifactId>
<version>7.2.3</version>
</dependency>
og følgende i din Javalin.create(..)
var app = Javalin.create(config -> {
//.. anden konfiguration
config.fileRenderer(new JavalinThymeleaf());
}).start(7070);
Thymeleaf templates ligger i:
src/main/resources/templates
En simpel template kan se sådan ud:
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<title>Min side</title>
</head>
<body>
<h1>Hej verden</h1>
</body>
</html>
Javalin kan rendre templaten:
ctx.render("index.html");
Når serveren renderer siden, bliver Thymeleaf-attributterne fortolket, og resultatet sendes som HTML til browseren.
Vi kan sende data til en template med ctx.attribute().
ctx.attribute("name", "Ada");
ctx.render("hello.html");
I templaten kan vi bruge attributten:
<h1 th:text="${name}">Navn</h1>
Browseren vil se:
<h1>Ada</h1>
th:text bruges til at sætte teksten i et HTML-element.
<p th:text="${name}">Navn</p>
Et objekt kan også bruges:
<p th:text="${user.username}">Brugernavn</p>
<p th:text="${user.email}">Email</p>
Thymeleaf kan bruge Java-objekters properties via deres getters.
Thymeleaf bruger ${...} til at hente værdier.
<p th:text="${name}"></p>
En property:
<p th:text="${user.username}"></p>
I praksis bruger vi normalt property-syntaksen:
${user.username}
Hvis controlleren sender en liste:
List<User> users = userService.getAllUsers();
ctx.attribute("users", users);
ctx.render("users.html");
kan Thymeleaf iterere over listen:
<ul>
<li th:each="user : ${users}"
th:text="${user.username}">
Brugernavn
</li>
</ul>
user er den aktuelle User.
users er listen.
Det svarer lidt til en Java for-each:
for (User user : users) {
// ...
}
Et tabel-eksempel:
<table>
<thead>
<tr>
<th>Brugernavn</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.username}">Brugernavn</td>
<td th:text="${user.email}">Email</td>
</tr>
</tbody>
</table>
Vi kan vise et element, hvis en betingelse er sand.
<p th:if="${user != null}">
Du er logget ind.
</p>
Hvis user er null, bliver elementet ikke vist.
Vi kan også kontrollere en liste:
<p th:if="${not #lists.isEmpty(users)}">
Der er brugere i systemet.
</p>
th:unless er den modsatte situation.
<p th:unless="${user != null}">
Du er ikke logget ind.
</p>
Det samme kan ofte skrives med th:if:
<p th:if="${user == null}">
Du er ikke logget ind.
</p>
Brug den form, der gør betingelsen lettest at forstå.
Links til Javalin-routes kan skrives med th:href.
<a th:href="@{/users}">
Brugere
</a>
Hvis URL’en indeholder en path-parameter:
<a th:href="@{/users/{id}(id=${user.id})}">
Vis bruger
</a>
Hvis user.id er 42, bliver URL’en:
/users/42
Query-parametre kan også bygges:
<a th:href="@{/users(group=1,page=2)}">
Vis brugere
</a>
Det giver:
/users?group=1&page=2
Billeder, CSS og andre static files placeres typisk i public.
src/main/resources
├── public
│ ├── css
│ └── images
└── templates
Et billede kan refereres sådan:
<img th:src="@{/images/logo.png}" alt="Logo">
Hvis filen ligger her:
src/main/resources/public/images/logo.png
bliver den tilgængelig på:
/images/logo.png
Et stylesheet i:
src/main/resources/public/css/styles.css
kan inkluderes med:
<link rel="stylesheet"
th:href="@{/css/styles.css}">
Data, som kun skal bruges under den aktuelle request, kan lægges i request scope:
ctx.attribute("name", "Ada");
ctx.render("index.html");
I Thymeleaf:
<h1 th:text="${name}">Navn</h1>
Request scope lever kun under den aktuelle request.
Et typisk eksempel:
List<Task> tasks = taskService.getTasksForUser(user);
ctx.attribute("tasks", tasks);
ctx.render("tasks.html");
Derefter:
<div th:each="task : ${tasks}">
<h2 th:text="${task.title}">Task</h2>
</div>
Session scope bruges til data, som skal kunne bruges på tværs af flere requests.
Et klassisk eksempel er den bruger, der er logget ind:
ctx.sessionAttribute("user", user);
På en senere request kan vi hente brugeren:
User user = ctx.sessionAttribute("user");
I Thymeleaf kan sessionens attribut bruges sådan:
<p th:text="${session.user.username}">
Brugernavn
</p>
Sessionen er derfor velegnet til eksempelvis:
- den aktuelle bruger
- login-status
- andre oplysninger, der skal huskes mellem requests
Et typisk flow ser sådan ud:
config.routes.get("/tasks", ctx -> {
User user = ctx.sessionAttribute("user");
List<Task> tasks = taskService.getTasksForUser(user);
ctx.attribute("tasks", tasks);
ctx.render("tasks.html");
});
Og templaten:
<h1>Mine tasks</h1>
<ul>
<li th:each="task : ${tasks}"
th:text="${task.title}">
Task
</li>
</ul>
Flowet er:
HTTP request
↓
Controller
↓
Service
↓
List<Task>
↓
ctx.attribute(...)
↓
Thymeleaf
↓
HTML
↓
Browser
Thymeleaf kan bruges sammen med almindelige HTML-formularer.
<form action="/login" method="post">
<label for="username">Brugernavn</label>
<input type="text"
id="username"
name="username">
<label for="password">Password</label>
<input type="password"
id="password"
name="password">
<button type="submit">
Log ind
</button>
</form>
Javalin kan hente værdierne:
String username = ctx.formParam("username");
String password = ctx.formParam("password");
Det vigtige er, at name i HTML matcher navnet i formParam().
Controlleren kan sende en fejlbesked til templaten:
ctx.attribute("error", "Forkert brugernavn eller password");
ctx.render("login.html");
I HTML:
<p th:if="${error != null}"
th:text="${error}">
Fejlbesked
</p>
Det gør det muligt at vise en fejl uden at lave en separat HTML-side.
Fragments bruges til genbrug af HTML.
Et fragment kan for eksempel indeholde en navigation:
<nav th:fragment="navigation">
<a th:href="@{/}">Forside</a>
<a th:href="@{/tasks}">Tasks</a>
<a th:href="@{/logout}">Log ud</a>
</nav>
Fragmentet kan indsættes i en anden template:
<div th:replace="~{fragments :: navigation}">
</div>
Hvis navigationen ændres, skal den derfor kun ændres ét sted.
Et fragment kan modtage parametre.
Fragment:
<head th:fragment="head(title)">
<title th:text="${title}">Titel</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
Brug fragmentet:
<div th:replace="~{fragments :: head('Mine tasks')}"></div>
Her sendes teksten Mine tasks ind som parameteren title.
En af Thymeleafs idéer er, at templates stadig er almindelig HTML.
Det betyder, at du kan skrive:
<h1 th:text="${name}">Navn</h1>
Hvis filen åbnes direkte i en browser uden Thymeleaf, ser man:
Navn
Når den renderes gennem Thymeleaf, bliver teksten erstattet med værdien af name.
Det kan gøre det lettere at arbejde med HTML, før backend-koden er færdig.
Fragments er en undtagelse, fordi browseren ikke selv kan fortolke Thymeleaf-fragments.
IntelliJ kan ikke altid vide, hvilke attributter controlleren sender til templaten.
Derfor kan autocomplete være begrænset.
Man kan hjælpe IntelliJ med en kommentar:
<!--/*@thymesVar id="user" type="entities.User"*/-->
Derefter kan IntelliJ bedre forstå:
<span th:text="${user.username}"></span>
Det er kun en hjælp til IDE’en. Kommentaren har ingen betydning for Thymeleaf.
| Jeg vil… | Jeg bruger… |
|---|---|
| Vise en værdi | th:text |
| Gå gennem en liste | th:each |
| Vise noget betinget | th:if |
| Vise noget hvis betingelsen er falsk | th:unless |
| Lave et link | th:href |
| Indsætte et billede | th:src |
| Sende data fra Java | ctx.attribute() |
| Gemme data i sessionen | ctx.sessionAttribute() |
| Vise en template | ctx.render() |
| Genbruge HTML | th:fragment + th:replace |
| Hente form-data | ctx.formParam() |
| Hente query-parametre | ctx.queryParam() |
Thymeleaf tager sig af præsentationen.
Controlleren henter data og beslutter, hvilken template der skal vises.
Servicen tager sig af programmets logik.
Browser
↓
Controller
↓
Service
↓
ctx.attribute(...)
↓
Thymeleaf
↓
HTML
↓
Browser