# Thymeleaf + HTMX Integration Patterns

Complete guide for integrating HTMX attributes with Thymeleaf templates.

## Table of Contents

1. [HTMX Namespace Setup](#htmx-namespace-setup)
2. [Dynamic Attributes](#dynamic-attributes)
3. [Fragment Patterns](#fragment-patterns)
4. [Form Handling](#form-handling)
5. [Conditional HTMX](#conditional-htmx)
6. [URL Building](#url-building)
7. [Response Targeting](#response-targeting)
8. [Common Patterns](#common-patterns)

---

## HTMX Namespace Setup

### Template Declaration

Add `hx` namespace to use dynamic HTMX attributes:

```html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  xmlns:hx="https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf"
>
  <head>
    <title>HTMX + Thymeleaf</title>
  </head>
  <body>
    <!-- Your content -->
  </body>
</html>
```

### Dependency Configuration

```xml
<dependency>
    <groupId>io.github.wimdeblauwe</groupId>
    <artifactId>htmx-spring-boot-thymeleaf</artifactId>
    <version>3.6.2</version>
</dependency>
```

---

## Dynamic Attributes

### Basic URL Building

Use `hx:get`, `hx:post`, etc. with Thymeleaf URL expressions:

```html
<!-- Dynamic hx-get -->
<button hx:get="@{/users/{id}(id=${user.id})}" hx-target="#details">
  View User
</button>

<!-- Dynamic hx-post -->
<form hx:post="@{/items}" hx-target="#item-list">
  <input name="name" required />
  <button type="submit">Add Item</button>
</form>

<!-- Dynamic hx-delete -->
<button
  hx:delete="@{/items/{id}(id=${item.id})}"
  hx-target="closest tr"
  hx-confirm="Delete this item?"
>
  Delete
</button>

<!-- Dynamic hx-put -->
<form
  hx:put="@{/users/{id}(id=${user.id})}"
  hx-target="#user-card"
  th:object="${user}"
>
  <input th:field="*{name}" />
  <button type="submit">Update</button>
</form>
```

### Query Parameters

Build URLs with query parameters:

```html
<!-- Static query params -->
<button hx:get="@{/search(query='test')}" hx-target="#results">
  Search "test"
</button>

<!-- Dynamic query params -->
<button
  hx:get="@{/users(page=${currentPage + 1},size=10)}"
  hx-target="#user-list"
  hx-swap="outerHTML"
>
  Next Page
</button>

<!-- Multiple dynamic params -->
<a
  hx:get="@{/search(query=${searchTerm},category=${category},sort='name')}"
  hx-target="#results"
>
  Search
</a>
```

### Path Variables

Multiple path variables:

```html
<!-- Single path variable -->
<button hx:get="@{/users/{id}/profile(id=${user.id})}">Profile</button>

<!-- Multiple path variables -->
<button
  hx:get="@{/organizations/{orgId}/users/{userId}(orgId=${org.id},userId=${user.id})}"
>
  View User
</button>

<!-- Combined path and query -->
<a
  hx:get="@{/categories/{id}/items(id=${category.id},page=${page},size=20)}"
  hx-target="#items"
>
  Load Items
</a>
```

### Dynamic hx-vals

Pass dynamic values as JSON:

```html
<!-- Static hx-vals -->
<button hx-post="/search" hx-vals='{"category": "books"}'>Search Books</button>

<!-- Dynamic hx-vals with Thymeleaf -->
<form hx-post="/search" hx:vals="${{query: searchTerm, category: category.id}}">
  <button type="submit">Search</button>
</form>

<!-- Complex dynamic vals -->
<button
  hx-get="/filter"
  hx:vals="${{
    minPrice: filters.minPrice,
    maxPrice: filters.maxPrice,
    tags: filters.tags,
    sort: 'price-asc'
  }}"
>
  Apply Filters
</button>

<!-- Including user context -->
<form
  hx-post="/cart/add"
  hx:vals="${{
    userId: user.id,
    productId: product.id,
    quantity: 1
  }}"
>
  <button type="submit">Add to Cart</button>
</form>
```

### Dynamic hx-headers

Set custom request headers:

```html
<!-- Static header -->
<div hx-get="/api/data" hx-headers='{"X-Custom-Header": "value"}'>Load</div>

<!-- Dynamic headers -->
<div
  hx-get="/api/data"
  hx:headers="${{
    'X-User-Id': user.id,
    'X-Session-Token': sessionToken
  }}"
>
  Load Data
</div>
```

---

## Fragment Patterns

### Fragment Definition

Define reusable HTMX-enabled fragments:

```html
<!-- fragments/user-card.html -->
<div th:fragment="card(user)" class="user-card" th:id="|user-card-${user.id}|">
  <h3 th:text="${user.name}">User Name</h3>
  <p th:text="${user.email}">user@example.com</p>

  <button
    hx:get="@{/users/{id}/edit(id=${user.id})}"
    hx:target="|#user-card-${user.id}|"
    hx-swap="outerHTML"
  >
    Edit
  </button>

  <button
    hx:delete="@{/users/{id}(id=${user.id})}"
    hx:target="|#user-card-${user.id}|"
    hx-swap="outerHTML swap:1s"
    hx-confirm="Delete this user?"
  >
    Delete
  </button>
</div>
```

### Fragment Usage

Include fragments with HTMX:

```html
<!-- List of user cards -->
<div id="user-list">
  <div
    th:each="user : ${users}"
    th:replace="~{fragments/user-card :: card(${user})}"
  ></div>
</div>

<!-- Add new user button -->
<button hx-get="/users/new" hx-target="#user-list" hx-swap="beforeend">
  Add User
</button>
```

### OOB Fragment

Fragments with out-of-band swap:

```html
<!-- fragments/cart-count.html -->
<span th:fragment="count" id="cart-count" hx-swap-oob="true">
  <span th:text="${cart.itemCount}">0</span> items
</span>

<!-- fragments/cart-total.html -->
<span th:fragment="total" id="cart-total" hx-swap-oob="true">
  $<span th:text="${#numbers.formatDecimal(cart.total, 2, 2)}">0.00</span>
</span>
```

**Controller returns multiple fragments:**

```java
@HxRequest
@PostMapping("/cart/add")
public View addToCart(@Valid CartItemForm form, Model model) {
    Cart cart = cartService.addItem(form);
    model.addAttribute("cart", cart);

    return FragmentsRendering
        .with("fragments/cart-count :: count")
        .fragment("fragments/cart-total :: total")
        .build();
}
```

### Parameterized Fragments

Fragments with multiple parameters:

```html
<!-- fragments/form-field.html -->
<div
  th:fragment="input(object, field, label, type, required)"
  class="form-group"
>
  <label th:for="${field}" th:text="${label}">Label</label>
  <input
    th:id="${field}"
    th:name="${field}"
    th:type="${type}"
    th:field="*{__${field}__}"
    th:classappend="${required} ? 'required' : ''"
    th:classappend="${#fields.hasErrors(field)} ? 'is-invalid' : ''"
  />
  <div th:if="${#fields.hasErrors(field)}" class="invalid-feedback">
    <span th:each="err : ${#fields.errors(field)}" th:text="${err}"></span>
  </div>
</div>
```

**Usage:**

```html
<form hx-post="/users" hx-target="#form-container">
  <div
    th:replace="~{fragments/form-field :: input(${user}, 'name', 'Full Name', 'text', true)}"
  ></div>
  <div
    th:replace="~{fragments/form-field :: input(${user}, 'email', 'Email', 'email', true)}"
  ></div>
  <div
    th:replace="~{fragments/form-field :: input(${user}, 'phone', 'Phone', 'tel', false)}"
  ></div>
  <button type="submit">Save</button>
</form>
```

---

## Form Handling

### Form with HTMX

Bind Thymeleaf form object with HTMX:

```html
<form
  hx:post="@{/users}"
  hx-target="#user-list"
  hx-swap="beforeend"
  th:object="${user}"
  hx-on::after-request="this.reset()"
>
  <div class="form-group">
    <label for="name">Name</label>
    <input
      type="text"
      id="name"
      th:field="*{name}"
      th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
    />
    <div th:if="${#fields.hasErrors('name')}" class="invalid-feedback">
      <span th:each="err : ${#fields.errors('name')}" th:text="${err}"></span>
    </div>
  </div>

  <div class="form-group">
    <label for="email">Email</label>
    <input
      type="email"
      id="email"
      th:field="*{email}"
      th:classappend="${#fields.hasErrors('email')} ? 'is-invalid' : ''"
    />
    <div th:if="${#fields.hasErrors('email')}" class="invalid-feedback">
      <span th:each="err : ${#fields.errors('email')}" th:text="${err}"></span>
    </div>
  </div>

  <button type="submit">Create User</button>
</form>
```

### Inline Validation

Validate fields as user types:

```html
<form hx-post="/users" th:object="${user}">
  <div class="form-group">
    <label for="email">Email</label>
    <input
      type="email"
      id="email"
      name="email"
      th:field="*{email}"
      hx-post="/validate/email"
      hx-trigger="blur, keyup changed delay:500ms"
      hx-target="next .validation-message"
      hx-sync="closest form:abort"
    />
    <div class="validation-message"></div>
  </div>

  <div class="form-group">
    <label for="username">Username</label>
    <input
      type="text"
      id="username"
      name="username"
      th:field="*{username}"
      hx-post="/validate/username"
      hx-trigger="blur, keyup changed delay:500ms"
      hx-target="next .validation-message"
    />
    <div class="validation-message"></div>
  </div>

  <button type="submit">Register</button>
</form>
```

### Select Dropdown Dynamic

Populate select options dynamically:

```html
<!-- Dependent dropdowns -->
<div class="form-group">
  <label for="country">Country</label>
  <select
    id="country"
    name="country"
    hx-get="/api/states"
    hx-target="#state-select"
    hx-trigger="change"
  >
    <option value="">Select Country</option>
    <option
      th:each="country : ${countries}"
      th:value="${country.code}"
      th:text="${country.name}"
      th:selected="${country.code == user.country}"
    >
      Country
    </option>
  </select>
</div>

<div class="form-group">
  <label for="state">State</label>
  <select id="state-select" name="state">
    <option value="">Select State</option>
    <!-- Populated by HTMX from /api/states -->
  </select>
</div>
```

### Checkbox/Radio with HTMX

```html
<!-- Checkboxes triggering filter update -->
<div class="filter-group">
  <label th:each="category : ${categories}">
    <input
      type="checkbox"
      name="categories"
      th:value="${category.id}"
      th:checked="${#lists.contains(selectedCategories, category.id)}"
      hx-get="/items/filter"
      hx-trigger="change"
      hx-target="#item-list"
      hx-include="[name='categories']:checked"
    />
    <span th:text="${category.name}">Category</span>
  </label>
</div>

<!-- Radio buttons -->
<div class="sort-options">
  <label th:each="option : ${sortOptions}">
    <input
      type="radio"
      name="sort"
      th:value="${option.value}"
      th:checked="${option.value == currentSort}"
      hx:get="@{/items(sort=${option.value})}"
      hx-trigger="change"
      hx-target="#item-list"
    />
    <span th:text="${option.label}">Sort Option</span>
  </label>
</div>
```

---

## Conditional HTMX

### Conditional Attributes

Enable HTMX only when conditions met:

```html
<!-- Only enable edit for active users -->
<button
  th:if="${user.isActive()}"
  hx:get="@{/users/{id}/edit(id=${user.id})}"
  hx-target="#user-card"
>
  Edit
</button>

<!-- Different actions based on status -->
<div th:switch="${order.status}">
  <button
    th:case="'PENDING'"
    hx:post="@{/orders/{id}/confirm(id=${order.id})}"
    hx-target="#order-status"
  >
    Confirm Order
  </button>

  <button
    th:case="'CONFIRMED'"
    hx:post="@{/orders/{id}/ship(id=${order.id})}"
    hx-target="#order-status"
  >
    Ship Order
  </button>

  <span th:case="'SHIPPED'">Order shipped</span>
</div>

<!-- Conditional auto-refresh -->
<div
  th:if="${task.isRunning()}"
  hx:get="@{/tasks/{id}/status(id=${task.id})}"
  hx-trigger="every 2s"
  hx-target="this"
  hx-swap="outerHTML"
>
  <p>Task running: <span th:text="${task.progress}">0</span>%</p>
</div>
```

### Conditional Triggers

Different triggers based on context:

```html
<!-- Auto-refresh only for pending items -->
<tr
  th:each="item : ${items}"
  th:attr="hx-get=${item.isPending()} ? @{/items/{id}(id=${item.id})} : null,
           hx-trigger=${item.isPending()} ? 'every 5s' : null,
           hx-target='this',
           hx-swap='outerHTML'"
>
  <td th:text="${item.name}">Item Name</td>
  <td th:text="${item.status}">Status</td>
</tr>
```

### Role-Based HTMX

Show/hide HTMX features by role:

```html
<html
  xmlns:th="http://www.thymeleaf.org"
  xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
  xmlns:hx="https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf"
>
  <!-- Admin-only HTMX actions -->
  <div sec:authorize="hasRole('ADMIN')">
    <button
      hx:delete="@{/users/{id}(id=${user.id})}"
      hx-target="closest tr"
      hx-confirm="Delete user?"
    >
      Delete User
    </button>
  </div>

  <!-- User can edit own profile -->
  <div sec:authorize="isAuthenticated()">
    <button
      th:if="${user.id == #authentication.principal.id}"
      hx:get="@{/users/{id}/edit(id=${user.id})}"
      hx-target="#profile-form"
    >
      Edit Profile
    </button>
  </div>

  <!-- Public view only -->
  <div sec:authorize="!isAuthenticated()">
    <p><a th:href="@{/login}">Login</a> to edit</p>
  </div>
</html>
```

---

## URL Building

### Complex URL Patterns

```html
<!-- Multiple query params -->
<a
  hx:get="@{/search(
    q=${query},
    category=${category.id},
    minPrice=${filters.minPrice},
    maxPrice=${filters.maxPrice},
    page=${page},
    sort='name'
  )}"
  hx-target="#results"
>
  Search
</a>

<!-- Encoded parameters -->
<button
  hx:get="@{/search(query=${#strings.urlEncode(searchTerm)})}"
  hx-target="#results"
>
  Search
</button>

<!-- Fragment identifier -->
<a
  hx:get="@{/users/{id}(id=${user.id}) + '#profile-section'}"
  hx-target="#content"
>
  View Profile
</a>
```

### Context-Relative URLs

```html
<!-- Context path auto-included -->
<button hx:get="@{/api/users}" hx-target="#users">Load Users</button>

<!-- External URL (not processed by @{}) -->
<div hx-get="https://api.example.com/data" hx-trigger="load">External Data</div>

<!-- Protocol-relative URL -->
<div hx-get="//cdn.example.com/data" hx-trigger="load">CDN Data</div>
```

### URL with Matrix Variables

```html
<!-- Matrix variables in path -->
<a
  hx:get="@{/items/{id};tags={tags}(id=${item.id},tags=${item.tags})}"
  hx-target="#item-details"
>
  View Item
</a>
```

---

## Response Targeting

### Dynamic Targets

```html
<!-- Target based on data -->
<button
  hx:get="@{/content}"
  th:attr="hx-target=${user.isAdmin()} ? '#admin-panel' : '#user-panel'"
>
  Load Content
</button>

<!-- Target using expression -->
<div
  th:each="item : ${items}"
  hx:get="@{/items/{id}(id=${item.id})}"
  th:attr="hx-target='#item-details-' + ${item.id}"
>
  <span th:text="${item.name}">Item</span>
</div>
<div th:each="item : ${items}" th:id="|item-details-${item.id}|">
  <!-- Details loaded here -->
</div>
```

### CSS Selector Targets

```html
<!-- Target closest parent -->
<button
  hx:delete="@{/items/{id}(id=${item.id})}"
  hx-target="closest tr"
  hx-swap="outerHTML swap:1s"
>
  Delete
</button>

<!-- Target sibling -->
<button hx-get="/edit-form" hx-target="next .form-container">Edit</button>

<!-- Target by class -->
<button hx-get="/notification" hx-target=".notification-area">
  Show Notification
</button>

<!-- Find in parent -->
<button hx-get="/status" hx-target="closest .card > .status">
  Update Status
</button>
```

### Multiple Targets with OOB

```html
<!-- Main response target + OOB updates -->
<form hx:post="@{/cart/add}" hx-target="#cart-items" hx-swap="beforeend">
  <input name="productId" th:value="${product.id}" type="hidden" />
  <button type="submit">Add to Cart</button>
</form>

<!-- These are updated via OOB from controller -->
<span id="cart-count">0 items</span>
<span id="cart-total">$0.00</span>
```

---

## Common Patterns

### Infinite Scroll

```html
<div id="items-container">
  <div th:each="item : ${items}" class="item">
    <h3 th:text="${item.name}">Item Name</h3>
  </div>

  <!-- Load more trigger -->
  <div
    th:if="${hasMore}"
    hx:get="@{/items(page=${nextPage})}"
    hx-trigger="revealed"
    hx-target="#items-container"
    hx-swap="beforeend"
  >
    <p>Loading more...</p>
  </div>
</div>
```

### Search with Debounce

```html
<form>
  <input
    type="search"
    name="q"
    placeholder="Search..."
    hx-get="/search"
    hx-trigger="keyup changed delay:500ms, search"
    hx-target="#search-results"
    hx-indicator="#search-spinner"
  />
  <span id="search-spinner" class="htmx-indicator">Searching...</span>
</form>

<div id="search-results">
  <!-- Results loaded here -->
</div>
```

### Polling/Auto-Refresh

```html
<!-- Poll every 10 seconds -->
<div
  id="status-panel"
  hx:get="@{/status}"
  hx-trigger="every 10s"
  hx-target="this"
  hx-swap="innerHTML"
>
  <p>Status: <span th:text="${status}">Unknown</span></p>
  <p>
    Last updated:
    <span th:text="${#dates.format(lastUpdate, 'HH:mm:ss')}"></span>
  </p>
</div>

<!-- Conditional polling -->
<div
  th:if="${job.isRunning()}"
  hx:get="@{/jobs/{id}(id=${job.id})}"
  hx-trigger="every 2s"
  hx-target="this"
  hx-swap="outerHTML"
>
  <p>Job progress: <span th:text="${job.progress}">0</span>%</p>
</div>
```

### Confirmation Dialog

```html
<!-- Built-in confirm -->
<button
  hx:delete="@{/items/{id}(id=${item.id})}"
  hx-target="closest tr"
  hx-swap="outerHTML swap:1s"
  th:attr="hx-confirm='Delete ' + ${item.name} + '?'"
>
  Delete
</button>

<!-- Custom confirm with event -->
<button
  hx:delete="@{/users/{id}(id=${user.id})}"
  hx-target="closest tr"
  hx-confirm="Are you sure? This cannot be undone."
>
  Delete User
</button>
```

### Loading States

```html
<!-- Using hx-indicator -->
<button
  hx-get="/slow-data"
  hx-target="#data-container"
  hx-indicator="#loading-spinner"
>
  Load Data
</button>
<div id="loading-spinner" class="htmx-indicator">Loading...</div>

<!-- Using CSS classes -->
<style>
  .htmx-request .htmx-indicator {
    display: inline;
  }
  .htmx-request.loading {
    opacity: 0.5;
    pointer-events: none;
  }
</style>

<div id="content" hx-get="/data" hx-trigger="load" class="loading">
  <span class="htmx-indicator">Loading...</span>
</div>
```

### Optimistic Updates

```html
<form
  hx:post="@{/items}"
  hx-target="#item-list"
  hx-swap="beforeend"
  hx-on::before-request="document.querySelector('#item-list').insertAdjacentHTML('beforeend', '<tr class=optimistic><td>Adding...</td></tr>')"
  hx-on::after-request="document.querySelector('.optimistic')?.remove()"
>
  <input name="name" required />
  <button type="submit">Add Item</button>
</form>
```

### Pagination

```html
<div id="items-container">
  <table>
    <tbody>
      <tr th:each="item : ${page.content}">
        <td th:text="${item.name}">Item</td>
      </tr>
    </tbody>
  </table>

  <nav th:if="${page.totalPages > 1}">
    <button
      th:if="${page.hasPrevious()}"
      hx:get="@{/items(page=${page.number - 1})}"
      hx-target="#items-container"
      hx-swap="outerHTML"
    >
      Previous
    </button>

    <span th:text="|Page ${page.number + 1} of ${page.totalPages}|"
      >Page 1 of 1</span
    >

    <button
      th:if="${page.hasNext()}"
      hx:get="@{/items(page=${page.number + 1})}"
      hx-target="#items-container"
      hx-swap="outerHTML"
    >
      Next
    </button>
  </nav>
</div>
```

---

## Best Practices

1. **Always use `hx:` namespace for dynamic URLs** - Ensures proper URL encoding and context path
2. **Combine static and dynamic attributes** - `hx-target` can be static while `hx:get` is dynamic
3. **Use th:attr for complex expressions** - When attribute value needs complex logic
4. **Fragment parameters for reusability** - Create parameterized fragments for common patterns
5. **Leverage Thymeleaf utilities** - Use `#strings`, `#dates`, `#numbers` for formatting
6. **Keep fragments focused** - Each fragment should have single responsibility
7. **Test fragment isolation** - Fragments should work independently
8. **Use OOB for multi-element updates** - More efficient than separate requests
9. **Implement proper loading states** - Always provide user feedback during requests
10. **Handle empty states** - Use `th:if="${#lists.isEmpty(items)}"` for better UX
