Skip to content

Commit

Permalink
st-tu-dresden#90 - Switch to HTMX for AJAX requests.
Browse files Browse the repository at this point in the history
  • Loading branch information
odrotbohm committed Oct 16, 2022
1 parent 85c2641 commit 96e58e1
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 109 deletions.
20 changes: 20 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot-thymeleaf</artifactId>
<version>2.0.0-M1</version>
</dependency>

<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId>
<version>1.8.2</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>hyperscript.org</artifactId>
<version>0.9.7</version>
<scope>runtime</scope>
</dependency>

<!-- DevTools -->

<dependency>
Expand Down
72 changes: 40 additions & 32 deletions src/main/java/guestbook/GuestbookController.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
*/
package guestbook;

import java.util.Optional;

import io.github.wimdeblauwe.hsbt.mvc.HtmxResponse;
import io.github.wimdeblauwe.hsbt.mvc.HxRequest;
import jakarta.validation.Valid;

import org.springframework.http.HttpEntity;
import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
Expand All @@ -43,9 +43,6 @@
@Controller
class GuestbookController {

// A special header sent with each AJAX request
private static final String IS_AJAX_HEADER = "X-Requested-With=XMLHttpRequest";

private final GuestbookRepository guestbook;

/**
Expand Down Expand Up @@ -110,27 +107,6 @@ String addEntry(@Valid @ModelAttribute("form") GuestbookForm form, Errors errors
return "redirect:/guestbook";
}

/**
* Handles AJAX requests to create a new {@link GuestbookEntry}. Instead of rendering a complete page, this view only
* renders and returns the HTML fragment representing the newly created entry.
* <p>
* Note that we do not react explicitly to a validation error: in such a case, Spring automatically returns an
* appropriate JSON document describing the error.
*
* @param form the form submitted by the user
* @param model the model that's used to render the view
* @return a reference to a Thymeleaf template fragment
* @see #addEntry(String, String)
*/
@PostMapping(path = "/guestbook", headers = IS_AJAX_HEADER)
String addEntry(@Valid GuestbookForm form, Model model) {

model.addAttribute("entry", guestbook.save(form.toNewEntry()));
model.addAttribute("index", guestbook.count());

return "guestbook :: entry";
}

/**
* Deletes a {@link GuestbookEntry}. This request can only be performed by authenticated users with admin privileges.
* Also note how the path variable used in the {@link DeleteMapping} annotation is bound to an {@link Optional}
Expand All @@ -152,22 +128,54 @@ String removeEntry(@PathVariable Optional<GuestbookEntry> entry) {
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}

// Request methods answering HTMX requests

/**
* Handles AJAX requests to create a new {@link GuestbookEntry}. Instead of rendering a complete page, this view only
* renders and returns the HTML fragment representing the newly created entry.
* <p>
* Note that we do not react explicitly to a validation error: in such a case, Spring automatically returns an
* appropriate JSON document describing the error.
*
* @param form the form submitted by the user
* @param model the model that's used to render the view
* @return a reference to a Thymeleaf template fragment
* @see #addEntry(String, String)
*/
@HxRequest
@PostMapping(path = "/guestbook")
HtmxResponse addEntry(@Valid GuestbookForm form, Model model) {

model.addAttribute("entry", guestbook.save(form.toNewEntry()));
model.addAttribute("index", guestbook.count());

return new HtmxResponse()
.addTemplate("guestbook :: entry")
.addTrigger("eventAdded");
}

/**
* Handles AJAX requests to delete {@link GuestbookEntry}s. Otherwise, this method is similar to
* {@link #removeEntry(Optional)}.
*
* @param entry an {@link Optional} with the {@link GuestbookEntry} to delete
* @return a response entity indicating success or failure of the removal
* @throws ResponseStatusException
*/
@HxRequest
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping(path = "/guestbook/{entry}", headers = IS_AJAX_HEADER)
HttpEntity<?> removeEntryJS(@PathVariable Optional<GuestbookEntry> entry) {
@DeleteMapping(path = "/guestbook/{entry}")
HtmxResponse removeEntryHtmx(@PathVariable Optional<GuestbookEntry> entry, Model model) {

return entry.map(it -> {

guestbook.delete(it);
return ResponseEntity.ok().build();

}).orElseGet(() -> ResponseEntity.notFound().build());
model.addAttribute("entries", guestbook.findAll());

return new HtmxResponse()
.addTemplate("guestbook :: entries");

}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
}
63 changes: 0 additions & 63 deletions src/main/resources/static/resources/js/guestbook.js

This file was deleted.

23 changes: 9 additions & 14 deletions src/main/resources/templates/guestbook.html
Original file line number Diff line number Diff line change
@@ -1,36 +1,32 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/resources/js/guestbook.js}"></script>
<script th:src="@{/webjars/htmx.org/dist/htmx.min.js}"></script>
<script th:src="@{/webjars/hyperscript.org/dist/_hyperscript.min.js}"></script>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" type="text/css" />
<link rel="stylesheet" th:href="@{/webjars/font-awesome/css/all.css}" type="text/css" />
<link rel="stylesheet" th:href="@{/resources/css/style.css}" type="text/css" />
<title th:text="#{guestbook.title}">Gästebuch</title>
</head>
<body>
<h1 class="text-center" th:text="#{guestbook.title}">Gästebuch</h1>

<div class="checkbox text-center">
<input type="checkbox" id="use_ajax" />
<label for="use_ajax" th:text="#{guestbook.useajax}">Ajax nutzen</label>
</div>


<div class="text-center">
<a sec:authorize="isAnonymous()" href="/login">Login</a>
<a sec:authorize="isAuthenticated()" href="/logout">Logout</a>
</div>

<br />

<div id="entries" class="mx-auto">
<div th:each="entry, it : ${entries}" th:with="index = ${it.count}">
<div th:each="entry, it : ${entries}" th:with="index = ${it.count}" th:fragment="entries">
<div class="card" th:fragment="entry" th:id="entry+${entry.id}">
<div class="card-header">
<form sec:authorize="hasRole('ADMIN')" th:method="delete" th:action="@{/guestbook/{id}(id=${entry.id})}" th:attr="data-entry-id=${entry.id}">
<button th:title="#{guestbook.form.delete}" class="btn btn-sm float-right">
<button th:title="#{guestbook.form.delete}" class="btn btn-sm float-right"
hx:delete="@{/guestbook/{id}(id=${entry.id})}"
hx:target="'#entries'">
<span class="fas fa-times"></span>
</button>
</form>
Expand All @@ -46,8 +42,7 @@ <h4 th:text="${index} + '. ' + ${entry.name}" class="card-title">1. Posting</h4>
</div>
</div>


<form method="post" role="form" class="gb-form" id="form" th:action="@{/guestbook}" th:object="${form}">
<form method="post" role="form" class="gb-form" id="form" hx:post="@{/guestbook}" hx-target="#entries" hx-swap="beforeend" _="on entryAdded me.reset()" th:action="@{/guestbook}" th:object="${form}">
<div class="form-group">
<label for="name" th:text="#{guestbook.form.name}">Name</label><br />
<input class="form-control" type="text" th:field="*{name}" th:errorclass="is-invalid" required="required" />
Expand Down

0 comments on commit 96e58e1

Please sign in to comment.