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('

' + data + '
'); - - // fix index - var index = $('#entries div[id^="entry"]').length; - var textArray = $(data).find('h3').text().split('.', 2); - - $('#entries div[id^="entry"]:last').find('h3').text(index + '.' + textArray[1]); - $('html, body').animate({scrollTop: form.offset().top}, 2000); - - e.target.reset(); - } - }); - }); - - $('#entries').on('submit','form', function(e){ - - if(!$('#use_ajax').is(':checked')) { - return; - } - - e.preventDefault(); - - var form = $(this); - var id = form.attr('data-entry-id'); - - $.ajax({ - type : 'DELETE', - cache : false, - url : form.attr('action'), - data : form.serialize(), - success : function() { - - $('#entry' + id).slideUp(500, function() { - var followingEntries = $(this).parent().nextAll().each(function() { - var textArray = $(this).find('h4').text().split('.', 2); - $(this).find('h4').text((parseInt(textArray[0],10)-1) + '.' + textArray[1]); - }); - - $(this).parent().remove(); - }); - } - }); - }); -}); diff --git a/src/main/resources/templates/guestbook.html b/src/main/resources/templates/guestbook.html index 444d2e5..b648518 100644 --- a/src/main/resources/templates/guestbook.html +++ b/src/main/resources/templates/guestbook.html @@ -1,10 +1,9 @@ - - - + + @@ -12,25 +11,22 @@

Gästebuch

- -
- - -
- +
Login Logout
- +
-
+
-
@@ -46,8 +42,7 @@

1. Posting

- -
+