Add thymeleaf-htmx module: Spring Boot 4.1.2 + Thymeleaf 3.1.5 + htmx 2.0.11 CRUD UI with fragment rendering, 422 validation re-render, pagination, inline edit, delete, MockMvc tests and a Chromium check
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# request (curl notation, sent through MockMvc): curl /contacts
|
||||
HTTP 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
---- body (3831 chars) ----
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Contacts</title>
|
||||
<!-- htmx 2 swaps 2xx/3xx only. 422 must be allowed to swap, or validation errors never appear. -->
|
||||
<meta name="htmx-config"
|
||||
content='{"responseHandling":[{"code":"204","swap":false},{"code":"[23]..","swap":true},{"code":"422","swap":true,"error":false},{"code":"[45]..","swap":false,"error":true},{"code":"...","swap":false}]}'>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<script src="/webjars/htmx.org/2.0.11/dist/htmx.min.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header><a href="/contacts">Contacts</a></header>
|
||||
<main>
|
||||
<h1>Contacts</h1>
|
||||
|
||||
<p><a href="/contacts/new">New contact</a></p>
|
||||
|
||||
<form action="/contacts" method="get" role="search">
|
||||
<input type="search" name="q" value="" placeholder="Search name or email"
|
||||
hx-get="/contacts" hx-trigger="input changed delay:300ms, search"
|
||||
hx-target="#contact-table" hx-swap="outerHTML" hx-push-url="true">
|
||||
<noscript><button type="submit">Search</button></noscript>
|
||||
</form>
|
||||
|
||||
<!-- th:fragment "table" is exactly what an htmx request gets back -->
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?page=0">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-1">
|
||||
<td>Aarav Kulkarni</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/1/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/1" hx-confirm="Delete Aarav Kulkarni?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-2">
|
||||
<td>Bhavna Joshi</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/2/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/2" hx-confirm="Delete Bhavna Joshi?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-3">
|
||||
<td>Chinmay Desai</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/3/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/3" hx-confirm="Delete Chinmay Desai?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-4">
|
||||
<td>Divya Nair</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Manager</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/4/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/4" hx-confirm="Delete Divya Nair?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-5">
|
||||
<td>Eshaan Patil</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/5/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/5" hx-confirm="Delete Eshaan Patil?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
|
||||
<span>Page 1 of 5 (23 contacts)</span>
|
||||
<a href="/contacts?page=1" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=1">Next »</a>
|
||||
</nav>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' /contacts
|
||||
HTTP 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
---- body (2598 chars) ----
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?page=0">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-1">
|
||||
<td>Aarav Kulkarni</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/1/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/1" hx-confirm="Delete Aarav Kulkarni?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-2">
|
||||
<td>Bhavna Joshi</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/2/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/2" hx-confirm="Delete Bhavna Joshi?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-3">
|
||||
<td>Chinmay Desai</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/3/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/3" hx-confirm="Delete Chinmay Desai?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-4">
|
||||
<td>Divya Nair</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Manager</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/4/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/4" hx-confirm="Delete Divya Nair?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-5">
|
||||
<td>Eshaan Patil</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/5/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/5" hx-confirm="Delete Eshaan Patil?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
|
||||
<span>Page 1 of 5 (23 contacts)</span>
|
||||
<a href="/contacts?page=1" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=1">Next »</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user