thymeleaf

Thymeleaf


Motivation

  • A und B loggen sich in WebUntis ein, beide sehen ihren Stundenplan
  • ⟹ dynamisches html
  • Thymeleaf ist eine template engine


Multi/Single Page Application

Multi
Flow durch Controller & mehrere html
Back-button ✔️
Single
Eine html Seite, Flow durch js (+ REST)
Back-button 🤔

Statisches html

  • sollte in resources\static\ liegen location
  • wird automatisch ausgeliefert

location


Spring MVC

mvc diagram


Controller

@Controller
@RequestMapping("/greetings")
public record GreetingController {

    @GetMapping("/greeting")
    public String greeting(
                @RequestParam(name = "name", 
                    required = false, 
                    defaultValue = "World") String name, 
                Model model) {
        model.addAttribute("name", name);
        return "greeting";
    }
}

Template

<!DOCTYPE HTML> <!-- greeting.html -->
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <title>Greetings</title>
    <meta http-equiv="Content-Type"
          content="text/html; charset=UTF-8"/>
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'"/>
</body>
</html>

${xxx} greift auf Modelattribut xxx zu

rendered


Views

@GetMapping(value = "/")
public String all(Model model) {
    model.addAttribute("students", studentRepository.findAll());
    return "student-list";
}
...
<head>,
<body>,
<table>
    <tr>
        <th>Name</th>
        <th>Id</th>
        <th>School</th>
    </tr>
    <tr th:each="student: ${students}">
        <td th:text="${student.id}">Id</td>
        <td>
            <a th:text="${student.name}"
               th:href="@{/students/{id} (id=${student.id})}">Name</a>
        </td>
        <td th:if="${student.school}"
            th:text="${student.school.name}">School
        </td>
    </tr>

Forms

@GetMapping(value = "/new")
public String enrollStudentForm(Model model) {
    model.addAttribute("student", new Student());
    model.addAttribute("interests", Interest.getCommons());
    model.addAttribute("schools", schoolRepository.findAll());
    return "student-enroll";
}

Form

...
<head>,
<body>
<form method="post"
      th:action="@{/students/new}"
// @ = baseurl
th:object="${student}"> // ${student.name} = *{name}
...
</form>

Text

...
<form th:object="${student}">
    <div>
        <label th:for="name">Name: </label>
        <input type="text" th:field="*{name}"/>
    </div>

Checkboxen

Sports
Movies
Programming
...
<form th:object="${student}">
    <div>
        <label>Interests: </label>
        <ul>
            <li th:each="interest: ${interests}">
                <input type="checkbox"
                       th:field="*{interests}"
                       th:value="${interest.id}"
                       th:text="${interest.description}"/>
            </li>
        </ul>
    </div>

Select

...
<form th:object="${student}">
    <div>
        <label th:for="school">School: </label>
        <select th:field="*{school}">
            <option th:each="school: ${schools}"
                    th:value="${school.id}"
                    th:text="${school.name}"></option>
        </select>
    </div>

Action-Endpoint

@PostMapping(value = "/new")
public String addStudent(@Valid Student student, 
        BindingResult bindingResult, Model model) {
    if (bindingResult.hasErrors()) {
        model.addAttribute("interests", Interest.getCommons());
        model.addAttribute("schools", schoolRepository.findAll());
        return "student-enroll";
    }
    var saved = studentRepository.save(student);
    return "redirect:/students/%d".formatted(saved.getId());
}

Fehler


<div th:if="${#fields.hasErrors('*')}">
    <h1>Errors</h1>
    <ul>
        <li th:each="err : ${#fields.errors('*')}"
            th:text="${err}">Error
        </li>
    </ul>
</div>

<div>
    <label for="name">Name: </label>
    <input type="text" id="name" th:field="*{name}"/>
    <span th:if="${#fields.hasErrors('name')}"
          th:errors="*{name}"/>
</div>

Fragments


<body>
<div th:insert="footer :: copy"></div>
<div th:replace="fragments/header :: header"></div>
</body>


    
Syntax: templatename :: selector
...

selector wie css


htmx - Motivation

Why should only <a> & <form> make HTTP requests?
Why should only click & submit events trigger them?
Why should only GET & POST methods be available?
Why can you only replace the entire screen?


htmx

<a href="/blog">Blog</a>

When a user clicks on this link, issue an HTTP GET request to ‘/blog’ and load the response content into the browser window.


<button hx-post="/clicked"
        hx-trigger="click"
        hx-target="#parent-div"
        hx-swap="outerHTML"
>

When a user clicks on this button, issue an HTTP POST request to ‘/clicked’ and use the content from the response to replace the element with the id parent-div


<head>
    <script src="https://unpkg.com/htmx.org@2.0.1">__SCRIPT_END__
</head>
<body>
<main class="container">
  <section>
    <h1>Htmx Demo</h1>
    <div id="parent-div"></div>
    <button hx-post="/clicked"
            hx-trigger="click"
            hx-target="#parent-div"
            hx-swap="outerHTML">
      Click Me!
    </button>
  </section>
</main>
</body>
@PostMapping("/clicked")
public String clicked(Model model) {
    model.addAttribute("now", LocalDateTime.now().toString());
    return "clicked :: result";
}

@PostMapping("/clicked")
public String clicked(Model model) {
    model.addAttribute("now", LocalDateTime.now().toString());
    return "clicked :: result";
}
<!-- clicked.html -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
  <title>fragments</title>
</head>
<body>
  <div th:fragment="result" id="parent-div">
    <p th:text="${now}"></p>
  </div>
</body>
</html>