diff --git a/pom.xml b/pom.xml
index 30424ee..bb8d235 100644
--- a/pom.xml
+++ b/pom.xml
@@ -106,6 +106,26 @@
runtime
+
+ io.github.wimdeblauwe
+ htmx-spring-boot-thymeleaf
+ 2.0.0-M1
+
+
+
+ org.webjars.npm
+ htmx.org
+ 1.8.2
+ runtime
+
+
+
+ org.webjars.npm
+ hyperscript.org
+ 0.9.7
+ runtime
+
+
diff --git a/src/main/java/guestbook/GuestbookController.java b/src/main/java/guestbook/GuestbookController.java
index f12aeb0..f2df447 100644
--- a/src/main/java/guestbook/GuestbookController.java
+++ b/src/main/java/guestbook/GuestbookController.java
@@ -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;
@@ -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;
/**
@@ -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.
- *
- * 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}
@@ -152,22 +128,54 @@ String removeEntry(@PathVariable Optional 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.
+ *
+ * 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 entry) {
+ @DeleteMapping(path = "/guestbook/{entry}")
+ HtmxResponse removeEntryHtmx(@PathVariable Optional 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));
}
}
diff --git a/src/main/resources/static/resources/js/guestbook.js b/src/main/resources/static/resources/js/guestbook.js
deleted file mode 100644
index a1bc67c..0000000
--- a/src/main/resources/static/resources/js/guestbook.js
+++ /dev/null
@@ -1,63 +0,0 @@
-$(document).ready(function() {
- 'use strict';
-
- $('#form').submit(function(e) {
-
- if(!$('#use_ajax').is(':checked')) {
- return;
- }
-
- e.preventDefault();
-
- var form = $(this);
-
- $.ajax({
- type : 'POST',
- cache : false,
- url : form.attr('action'),
- data : form.serialize(),
- success : function(data) {
- $("#entries").append('