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 @@
|
||||
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.50 s -- in com.ankurm.thymeleafhtmx.ContactUiTest
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# htmx sends BOTH headers when the history cache misses; it swaps the reply into <body>
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' -H 'HX-History-Restore-Request: true' '/contacts?page=1'
|
||||
HTTP 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
---- body (3937 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=1">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-6">
|
||||
<td>Farah Sheikh</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/6/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/6" hx-confirm="Delete Farah Sheikh?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-7">
|
||||
<td>Gauri Deshpande</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/7/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/7" hx-confirm="Delete Gauri Deshpande?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-8">
|
||||
<td>Harsh Mehta</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Manager</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/8/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/8" hx-confirm="Delete Harsh Mehta?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-9">
|
||||
<td>Isha Rao</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/9/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/9" hx-confirm="Delete Isha Rao?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-10">
|
||||
<td>Jayesh Pawar</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/10/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/10" hx-confirm="Delete Jayesh Pawar?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
<a href="/contacts?page=0" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=0">« Prev</a>
|
||||
<span>Page 2 of 5 (23 contacts)</span>
|
||||
<a href="/contacts?page=2" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=2">Next »</a>
|
||||
</nav>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# script tag as rendered (the template asks for /webjars/htmx.org/dist/htmx.min.js, no version)
|
||||
# <script src="/webjars/htmx.org/2.0.11/dist/htmx.min.js" defer></script>
|
||||
# GET /webjars/htmx.org/2.0.11/dist/htmx.min.js -> 200, 52182 bytes, Content-Type text/javascript
|
||||
@@ -0,0 +1,138 @@
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' '/contacts?page=4'
|
||||
HTTP 200
|
||||
---- body (1763 chars) ----
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?page=4">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-21">
|
||||
<td>Uma Pillai</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/21/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/21" hx-confirm="Delete Uma Pillai?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-22">
|
||||
<td>Vikram Singh</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/22/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/22" hx-confirm="Delete Vikram Singh?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-23">
|
||||
<td>Yash Thakur</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/23/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/23" hx-confirm="Delete Yash Thakur?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
<a href="/contacts?page=3" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=3">« Prev</a>
|
||||
<span>Page 5 of 5 (23 contacts)</span>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' '/contacts?page=99'
|
||||
HTTP 200
|
||||
---- body (1763 chars) ----
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?page=4">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-21">
|
||||
<td>Uma Pillai</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/21/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/21" hx-confirm="Delete Uma Pillai?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-22">
|
||||
<td>Vikram Singh</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/22/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/22" hx-confirm="Delete Vikram Singh?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-23">
|
||||
<td>Yash Thakur</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/23/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/23" hx-confirm="Delete Yash Thakur?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
<a href="/contacts?page=3" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=3">« Prev</a>
|
||||
<span>Page 5 of 5 (23 contacts)</span>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' '/contacts?q=KAMAT'
|
||||
HTTP 200
|
||||
---- body (828 chars) ----
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?q=KAMAT&page=0">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-19">
|
||||
<td>Sneha Kamat</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/19/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/19" hx-confirm="Delete Sneha Kamat?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
|
||||
<span>Page 1 of 1 (1 contacts)</span>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' -d 'name=&email=not-an-email&role=Developer' /contacts
|
||||
HTTP 422
|
||||
---- body (636 chars) ----
|
||||
<form id="contact-form" action="/contacts" method="post"
|
||||
hx-post="/contacts" hx-target="this" hx-swap="outerHTML">
|
||||
<p>
|
||||
<label>Name <input id="name" name="name" value="" class="invalid"></label>
|
||||
<small class="error">Name is required</small>
|
||||
</p>
|
||||
<p>
|
||||
<label>Email <input id="email" name="email" value="not-an-email" class="invalid"></label>
|
||||
<small class="error">Email must look like name@host</small>
|
||||
</p>
|
||||
<p>
|
||||
<label>Role <input id="role" name="role" value="Developer"></label>
|
||||
|
||||
</p>
|
||||
<button type="submit">Create</button>
|
||||
<a href="/contacts">Cancel</a>
|
||||
</form>
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -d 'name=&[email protected]&role=Tester' /contacts (no JavaScript, no HX header)
|
||||
HTTP 422
|
||||
---- body (1461 chars) ----
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>New contact</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>New contact</h1>
|
||||
|
||||
<!-- With JS: hx-post swaps this form for the re-rendered one on a 422.
|
||||
Without JS: the same form posts normally and the full page comes back. -->
|
||||
<form id="contact-form" action="/contacts" method="post"
|
||||
hx-post="/contacts" hx-target="this" hx-swap="outerHTML">
|
||||
<p>
|
||||
<label>Name <input id="name" name="name" value="" class="invalid"></label>
|
||||
<small class="error">Name is required</small>
|
||||
</p>
|
||||
<p>
|
||||
<label>Email <input id="email" name="email" value="[email protected]"></label>
|
||||
|
||||
</p>
|
||||
<p>
|
||||
<label>Role <input id="role" name="role" value="Tester"></label>
|
||||
|
||||
</p>
|
||||
<button type="submit">Create</button>
|
||||
<a href="/contacts">Cancel</a>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' -d 'name=Zoya Khan&[email protected]&role=Developer' /contacts
|
||||
HTTP 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
HX-Redirect: /contacts
|
||||
---- body (0 chars) ----
|
||||
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -d 'name=Zubin Irani&[email protected]&role=Tester' /contacts
|
||||
HTTP 302
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
Location: /contacts
|
||||
---- body (0 chars) ----
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' /contacts/1/edit (Edit button)
|
||||
HTTP 200
|
||||
---- body (523 chars) ----
|
||||
<tr id="contact-1" class="editing">
|
||||
<td>
|
||||
<input name="name" value="Aarav Kulkarni">
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<input name="email" value="[email protected]">
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<input name="role" value="Developer">
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-include="closest tr" hx-target="closest tr" hx-swap="outerHTML" hx-put="/contacts/1">Save</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/1">Cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -X PUT -H 'HX-Request: true' -d 'name=&email=broken&role=Architect' /contacts/1 (Save, invalid)
|
||||
HTTP 422
|
||||
---- body (625 chars) ----
|
||||
<tr id="contact-1" class="editing">
|
||||
<td>
|
||||
<input name="name" value="" class="invalid">
|
||||
<small class="error">Name is required</small>
|
||||
</td>
|
||||
<td>
|
||||
<input name="email" value="broken" class="invalid">
|
||||
<small class="error">Email must look like name@host</small>
|
||||
</td>
|
||||
<td>
|
||||
<input name="role" value="Architect">
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-include="closest tr" hx-target="closest tr" hx-swap="outerHTML" hx-put="/contacts/1">Save</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/1">Cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -X PUT -H 'HX-Request: true' -d 'name=Aarav K.&[email protected]&role=Architect' /contacts/1 (Save, valid)
|
||||
HTTP 200
|
||||
---- body (382 chars) ----
|
||||
<tr id="contact-1">
|
||||
<td>Aarav K.</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</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 K.?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# request (curl notation, sent through MockMvc): curl -X DELETE -H 'HX-Request: true' /contacts/1
|
||||
HTTP 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
HX-Trigger: contactsChanged
|
||||
---- body (0 chars) ----
|
||||
|
||||
|
||||
# request (curl notation, sent through MockMvc): curl -X DELETE -H 'HX-Request: true' /contacts/999999
|
||||
HTTP 404
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
---- body (0 chars) ----
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# deleted the 3 rows on page 5, then the table (which re-fetches page 5 after HX-Trigger) asks for page index 4
|
||||
# request (curl notation, sent through MockMvc): curl -H 'HX-Request: true' '/contacts?page=4'
|
||||
HTTP 200
|
||||
---- body (2602 chars) ----
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?page=3">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Role</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
<tr id="contact-16">
|
||||
<td>Pooja Verma</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Manager</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/16/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/16" hx-confirm="Delete Pooja Verma?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-17">
|
||||
<td>Qasim Ansari</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Developer</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/17/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/17" hx-confirm="Delete Qasim Ansari?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-18">
|
||||
<td>Rohan Sawant</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Tester</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/18/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/18" hx-confirm="Delete Rohan Sawant?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-19">
|
||||
<td>Sneha Kamat</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Architect</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/19/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/19" hx-confirm="Delete Sneha Kamat?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="contact-20">
|
||||
<td>Tanmay Ghosh</td>
|
||||
<td>[email protected]</td>
|
||||
<td>Manager</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-get="/contacts/20/edit">Edit</button>
|
||||
<button type="button"
|
||||
hx-target="closest tr" hx-swap="outerHTML" hx-delete="/contacts/20" hx-confirm="Delete Tanmay Ghosh?">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager" aria-label="Pages">
|
||||
<a href="/contacts?page=2" hx-target="#contact-table" hx-push-url="true" hx-get="/contacts?page=2">« Prev</a>
|
||||
<span>Page 4 of 4 (20 contacts)</span>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
$ curl -si http://localhost:18082/contacts | tr -d '\r' | grep -v -E '^(Date|Transfer-Encoding)' | head -10
|
||||
HTTP/1.1 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
Content-Type: text/html;charset=UTF-8
|
||||
Content-Language: en-US
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
$ curl -si -H 'HX-Request: true' http://localhost:18082/contacts | tr -d '\r' | grep -v -E '^(Date|Transfer-Encoding)' | head -8
|
||||
HTTP/1.1 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
Content-Type: text/html;charset=UTF-8
|
||||
Content-Language: en-US
|
||||
|
||||
<div id="contact-table" hx-trigger="contactsChanged from:body" hx-target="this" hx-swap="outerHTML" hx-get="/contacts?page=0">
|
||||
<table>
|
||||
|
||||
$ curl -s -o /dev/null -w '%{http_code} %{content_type}\n' http://localhost:18082/webjars/htmx.org/dist/htmx.min.js
|
||||
200 text/javascript
|
||||
|
||||
$ curl -si -X DELETE -H 'HX-Request: true' http://localhost:18082/contacts/23
|
||||
HTTP/1.1 200
|
||||
Vary: HX-Request
|
||||
Vary: HX-History-Restore-Request
|
||||
HX-Trigger: contactsChanged
|
||||
Content-Length: 0
|
||||
|
||||
|
||||
$ curl -si -X DELETE -H 'HX-Request: true' http://localhost:18082/contacts/23 (again)
|
||||
HTTP/1.1 404
|
||||
@@ -0,0 +1,52 @@
|
||||
== 1. load, then search without a page reload
|
||||
htmx loaded: object 2.0.11
|
||||
rows on first page: 5
|
||||
after typing "kamat": ["Sneha Kamat"]
|
||||
address bar: /contacts?q=kamat
|
||||
elements with id contact-table: 1
|
||||
document survived (no reload): same-document
|
||||
|
||||
== 2. pagination + back button
|
||||
page 2 names: ["Farah Sheikh","Gauri Deshpande","Harsh Mehta","Isha Rao","Jayesh Pawar"]
|
||||
address bar: /contacts?page=1
|
||||
Back request: GET /contacts?q= HX-Request=true HX-History-Restore-Request=true
|
||||
after Back: h1="Contacts", pager="Page 1 of 5 (23 contacts)"
|
||||
|
||||
== 3. inline edit with a validation error
|
||||
row still editing, errors: ["Name is required","Email must look like name@host"]
|
||||
saved, row is read-only again: ["Aarav K.","Bhavna Joshi","Chinmay Desai","Divya Nair","Eshaan Patil"]
|
||||
|
||||
== 4. delete with confirm
|
||||
confirm dialog: "Delete Aarav K.?"
|
||||
pager after delete: "Page 1 of 5 (22 contacts)"
|
||||
requests caused by one delete: ["DELETE /contacts/1 HX-Request=true HX-History-Restore-Request=-","GET /contacts?page=0 HX-Request=true HX-History-Restore-Request=-"]
|
||||
|
||||
== 5. create form: 422 re-render, then success
|
||||
errors shown in place: ["Name is required","Email must look like name@host"]
|
||||
still on: /contacts/new, typed email kept: x
|
||||
HX-Redirect landed on: /contacts
|
||||
|
||||
== 6. the two status-code traps, observed
|
||||
DELETE answered 204: rows before=5, after=5 (row not removed)
|
||||
htmx-config meta present: 0
|
||||
Save answered 422 without the meta tag: errors visible = 0
|
||||
|
||||
== requests the browser made (excluding static files)
|
||||
GET /contacts HX-Request=- HX-History-Restore-Request=-
|
||||
GET /contacts?q=kamat HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts?q= HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts?page=1 HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts?q= HX-Request=true HX-History-Restore-Request=true
|
||||
GET /contacts/1/edit HX-Request=true HX-History-Restore-Request=-
|
||||
PUT /contacts/1 HX-Request=true HX-History-Restore-Request=-
|
||||
PUT /contacts/1 HX-Request=true HX-History-Restore-Request=-
|
||||
DELETE /contacts/1 HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts?page=0 HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts/new HX-Request=- HX-History-Restore-Request=-
|
||||
POST /contacts HX-Request=true HX-History-Restore-Request=-
|
||||
POST /contacts HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts HX-Request=- HX-History-Restore-Request=-
|
||||
DELETE /contacts/2 HX-Request=true HX-History-Restore-Request=-
|
||||
GET /contacts HX-Request=- HX-History-Restore-Request=-
|
||||
GET /contacts/2/edit HX-Request=true HX-History-Restore-Request=-
|
||||
PUT /contacts/2 HX-Request=true HX-History-Restore-Request=-
|
||||
Reference in New Issue
Block a user