# HTMX Integration Patterns - Public Web Frontend

**Last Updated:** November 18, 2025

---

## Overview

This document outlines HTMX usage patterns, best practices, and common implementations for the Code of Conquest web frontend.

**HTMX Benefits:**
- Server-side rendering with dynamic interactions
- No JavaScript framework overhead
- Progressive enhancement (works without JS)
- Clean separation of concerns
- Natural integration with Flask/Jinja2

---

## Core HTMX Attributes

### hx-get / hx-post / hx-put / hx-delete

Make HTTP requests from HTML elements:

```html
<!-- GET request -->
<button hx-get="/api/v1/characters" hx-target="#characters">
    Load Characters
</button>

<!-- POST request -->
<form hx-post="/api/v1/characters" hx-target="#result">
    <!-- form fields -->
    <button type="submit">Create</button>
</form>

<!-- DELETE request -->
<button hx-delete="/api/v1/characters/{{ character_id }}"
        hx-target="closest .character-card"
        hx-swap="outerHTML">
    Delete
</button>
```

### hx-target

Specify where to insert the response:

```html
<!-- CSS selector -->
<button hx-get="/content" hx-target="#content-div">Load</button>

<!-- Relative selectors -->
<button hx-delete="/item" hx-target="closest .item">Delete</button>
<button hx-post="/append" hx-target="next .list">Add</button>

<!-- Special values -->
<button hx-get="/modal" hx-target="body">Show Modal</button>
```

### hx-swap

Control how content is swapped:

```html
<!-- innerHTML (default) -->
<div hx-get="/content" hx-target="#div" hx-swap="innerHTML"></div>

<!-- outerHTML (replace element itself) -->
<div hx-get="/content" hx-target="#div" hx-swap="outerHTML"></div>

<!-- beforebegin (before target) -->
<div hx-get="/content" hx-target="#div" hx-swap="beforebegin"></div>

<!-- afterbegin (first child) -->
<div hx-get="/content" hx-target="#div" hx-swap="afterbegin"></div>

<!-- beforeend (last child) -->
<div hx-get="/content" hx-target="#div" hx-swap="beforeend"></div>

<!-- afterend (after target) -->
<div hx-get="/content" hx-target="#div" hx-swap="afterend"></div>

<!-- none (no swap, just trigger) -->
<div hx-get="/trigger" hx-swap="none"></div>
```

### hx-trigger

Specify what triggers the request:

```html
<!-- Click (default for buttons) -->
<button hx-get="/content">Click Me</button>

<!-- Change (default for inputs) -->
<select hx-get="/filter" hx-trigger="change">...</select>

<!-- Custom events -->
<div hx-get="/content" hx-trigger="customEvent">...</div>

<!-- Multiple triggers -->
<div hx-get="/content" hx-trigger="mouseenter, focus">...</div>

<!-- Trigger modifiers -->
<input hx-get="/search"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#results">

<!-- Polling -->
<div hx-get="/status" hx-trigger="every 5s">Status: ...</div>

<!-- Load once -->
<div hx-get="/content" hx-trigger="load">...</div>
```

---

## Common Patterns

### Form Submission

**Pattern:** Submit form via AJAX, replace form with result

```html
<form hx-post="/api/v1/characters"
      hx-target="#form-container"
      hx-swap="outerHTML">

    <input type="text" name="name" placeholder="Character Name" required>

    <select name="class">
        <option value="vanguard">Vanguard</option>
        <option value="luminary">Luminary</option>
    </select>

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

**Backend Response:**
```python
@characters_bp.route('/', methods=['POST'])
def create_character():
    # Create character via API
    response = api_client.post('/characters', request.form)

    if response.status_code == 200:
        character = response.json()['result']
        return render_template('partials/character_card.html', character=character)
    else:
        return render_template('partials/form_error.html', error=response.json()['error'])
```

### Delete with Confirmation

**Pattern:** Confirm before deleting, remove element on success

```html
<div class="character-card" id="char-{{ character.character_id }}">
    <h3>{{ character.name }}</h3>

    <button hx-delete="/api/v1/characters/{{ character.character_id }}"
            hx-confirm="Are you sure you want to delete {{ character.name }}?"
            hx-target="closest .character-card"
            hx-swap="outerHTML swap:1s">
        Delete
    </button>
</div>
```

**Backend Response (empty for delete):**
```python
@characters_bp.route('/<character_id>', methods=['DELETE'])
def delete_character(character_id):
    api_client.delete(f'/characters/{character_id}')
    return '', 200  # Empty response removes element
```

### Search/Filter

**Pattern:** Live search with debouncing

```html
<input type="text"
       name="search"
       placeholder="Search characters..."
       hx-get="/characters/search"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#character-list"
       hx-indicator="#search-spinner">

<span id="search-spinner" class="htmx-indicator">Searching...</span>

<div id="character-list">
    <!-- Results appear here -->
</div>
```

**Backend:**
```python
@characters_bp.route('/search')
def search_characters():
    query = request.args.get('search', '')
    characters = api_client.get(f'/characters?search={query}')
    return render_template('partials/character_list.html', characters=characters)
```

### Pagination

**Pattern:** Load more items

```html
<div id="character-list">
    {% for character in characters %}
        {% include 'partials/character_card.html' %}
    {% endfor %}
</div>

{% if has_more %}
<button hx-get="/characters?page={{ page + 1 }}"
        hx-target="#character-list"
        hx-swap="beforeend"
        hx-select=".character-card">
    Load More
</button>
{% endif %}
```

### Inline Edit

**Pattern:** Click to edit, save inline

```html
<div class="character-name" id="name-{{ character.character_id }}">
    <span hx-get="/characters/{{ character.character_id }}/edit-name"
          hx-target="closest div"
          hx-swap="outerHTML">
        {{ character.name }} ✏️
    </span>
</div>
```

**Edit Form Response:**
```html
<div class="character-name" id="name-{{ character.character_id }}">
    <form hx-put="/characters/{{ character.character_id }}/name"
          hx-target="closest div"
          hx-swap="outerHTML">
        <input type="text" name="name" value="{{ character.name }}" autofocus>
        <button type="submit">Save</button>
        <button type="button" hx-get="/characters/{{ character.character_id }}/name" hx-target="closest div" hx-swap="outerHTML">Cancel</button>
    </form>
</div>
```

### Modal Dialog

**Pattern:** Load modal content dynamically

```html
<button hx-get="/characters/{{ character.character_id }}/details"
        hx-target="#modal-container"
        hx-swap="innerHTML">
    View Details
</button>

<div id="modal-container"></div>
```

**Modal Response:**
```html
<div class="modal" id="character-modal">
    <div class="modal-content">
        <span class="close" onclick="closeModal()">&times;</span>
        <h2>{{ character.name }}</h2>
        <!-- character details -->
    </div>
</div>

<script>
function closeModal() {
    document.getElementById('character-modal').remove();
}
</script>
```

### Tabs

**Pattern:** Tab switching without page reload

```html
<div class="tabs">
    <button hx-get="/dashboard/overview"
            hx-target="#tab-content"
            class="tab active">
        Overview
    </button>
    <button hx-get="/dashboard/characters"
            hx-target="#tab-content"
            class="tab">
        Characters
    </button>
    <button hx-get="/dashboard/sessions"
            hx-target="#tab-content"
            class="tab">
        Sessions
    </button>
</div>

<div id="tab-content">
    <!-- Tab content loads here -->
</div>
```

### Polling for Updates

**Pattern:** Auto-refresh session status

```html
<div hx-get="/sessions/{{ session_id }}/status"
     hx-trigger="every 5s"
     hx-target="#session-status">
    Loading status...
</div>
```

**Conditional Polling (stop when complete):**
```html
<div hx-get="/sessions/{{ session_id }}/status"
     hx-trigger="every 5s[status !== 'completed']"
     hx-target="#session-status">
    Status: {{ session.status }}
</div>
```

### Infinite Scroll

**Pattern:** Load more as user scrolls

```html
<div id="character-list">
    {% for character in characters %}
        {% include 'partials/character_card.html' %}
    {% endfor %}
</div>

{% if has_more %}
<div hx-get="/characters?page={{ page + 1 }}"
     hx-trigger="revealed"
     hx-target="#character-list"
     hx-swap="beforeend">
    Loading more...
</div>
{% endif %}
```

---

## Advanced Patterns

### Dependent Dropdowns

**Pattern:** Update second dropdown based on first selection

```html
<select name="class"
        hx-get="/characters/abilities"
        hx-target="#ability-select"
        hx-trigger="change">
    <option value="vanguard">Vanguard</option>
    <option value="luminary">Luminary</option>
</select>

<div id="ability-select">
    <!-- Abilities dropdown loads here based on class -->
</div>
```

### Out of Band Swaps

**Pattern:** Update multiple areas from single response

```html
<button hx-post="/combat/attack"
        hx-target="#combat-log"
        hx-swap="beforeend">
    Attack
</button>

<div id="combat-log"><!-- Combat log --></div>
<div id="character-hp">HP: 50/100</div>
```

**Backend Response:**
```html
<!-- Primary swap target -->
<div>You dealt 15 damage!</div>

<!-- Out of band swap -->
<div id="character-hp" hx-swap-oob="true">HP: 45/100</div>
```

### Optimistic UI

**Pattern:** Show result immediately, revert on error

```html
<button hx-post="/characters/{{ character_id }}/favorite"
        hx-target="#favorite-btn"
        hx-swap="outerHTML"
        class="btn">
    ⭐ Favorite
</button>
```

**Immediate feedback with CSS:**
```css
.htmx-request .htmx-indicator {
    display: inline;
}
```

### Progressive Enhancement

**Pattern:** Fallback to normal form submission

```html
<form action="/characters" method="POST"
      hx-post="/characters"
      hx-target="#result">
    <!-- If HTMX fails, form still works via normal POST -->
    <input type="text" name="name">
    <button type="submit">Create</button>
</form>
```

---

## HTMX + Appwrite Realtime

### Hybrid Approach

Use HTMX for user actions, Appwrite Realtime for server push updates:

```html
<!-- HTMX for user actions -->
<button hx-post="/sessions/{{ session_id }}/ready"
        hx-target="#lobby-status">
    Ready
</button>

<!-- Appwrite Realtime for live updates -->
<script>
const client = new Appwrite.Client()
    .setEndpoint('{{ config.appwrite_endpoint }}')
    .setProject('{{ config.appwrite_project_id }}');

client.subscribe('channel', response => {
    // Trigger HTMX to reload specific section
    htmx.ajax('GET', '/sessions/{{ session_id }}/status', {
        target: '#session-status',
        swap: 'innerHTML'
    });
});
</script>
```

---

## Error Handling

### Show Error Messages

```html
<form hx-post="/characters"
      hx-target="#form-container"
      hx-target-error="#error-container">
    <!-- form fields -->
</form>

<div id="error-container"></div>
```

**Backend Error Response:**
```python
@characters_bp.route('/', methods=['POST'])
def create_character():
    try:
        # Create character
        pass
    except ValidationError as e:
        response = make_response(render_template('partials/error.html', error=str(e)), 400)
        response.headers['HX-Retarget'] = '#error-container'
        return response
```

### Retry on Failure

```html
<div hx-get="/api/v1/data"
     hx-trigger="load, error from:body delay:5s">
    Loading...
</div>
```

---

## Loading Indicators

### Global Indicator

```html
<style>
.htmx-indicator {
    display: none;
}
.htmx-request .htmx-indicator {
    display: inline;
}
</style>

<button hx-post="/action" hx-indicator="#spinner">
    Submit
</button>
<span id="spinner" class="htmx-indicator">Loading...</span>
```

### Inline Indicator

```html
<button hx-get="/content">
    <span class="button-text">Load</span>
    <span class="htmx-indicator">Loading...</span>
</button>
```

---

## Best Practices

### 1. Use Semantic HTML
```html
<!-- Good -->
<button hx-post="/action">Submit</button>

<!-- Avoid -->
<div hx-post="/action">Submit</div>
```

### 2. Provide Fallbacks
```html
<form action="/submit" method="POST" hx-post="/submit">
    <!-- Works without JavaScript -->
</form>
```

### 3. Use hx-indicator for Loading States
```html
<button hx-post="/action" hx-indicator="#spinner">
    Submit
</button>
<span id="spinner" class="htmx-indicator">⏳</span>
```

### 4. Debounce Search Inputs
```html
<input hx-get="/search"
       hx-trigger="keyup changed delay:500ms">
```

### 5. Use CSRF Protection
```html
<form hx-post="/action">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form>
```

---

## Debugging

### HTMX Events

Listen to HTMX events for debugging:

```html
<script>
document.body.addEventListener('htmx:beforeRequest', (event) => {
    console.log('Before request:', event.detail);
});

document.body.addEventListener('htmx:afterRequest', (event) => {
    console.log('After request:', event.detail);
});

document.body.addEventListener('htmx:responseError', (event) => {
    console.error('Response error:', event.detail);
});
</script>
```

### HTMX Logger Extension

```html
<script src="https://unpkg.com/htmx.org/dist/ext/debug.js"></script>
<body hx-ext="debug">
```

---

## Performance Tips

### 1. Use hx-select to Extract Partial
```html
<button hx-get="/full-page"
        hx-select="#content-section"
        hx-target="#result">
    Load Section
</button>
```

### 2. Disable During Request
```html
<button hx-post="/action" hx-disable-during-request>
    Submit
</button>
```

### 3. Use hx-sync for Sequential Requests
```html
<div hx-sync="this:replace">
    <button hx-get="/content">Load</button>
</div>
```

---

## Related Documentation

- **[TEMPLATES.md](TEMPLATES.md)** - Template structure and conventions
- **[TESTING.md](TESTING.md)** - Manual testing guide
- **[/api/docs/API_REFERENCE.md](../../api/docs/API_REFERENCE.md)** - API endpoints
- **[HTMX Official Docs](https://htmx.org/docs/)** - Complete HTMX documentation

---

**Document Version:** 1.1
**Created:** November 18, 2025
**Last Updated:** November 21, 2025
