HTMX

Erstellt: 2025-12-27 | Aktualisiert: 2025-12-31

HTMX ist die einzige erlaubte Methode für AJAX-Interaktionen im Campus-System.

Warum HTMX?

AspektHTMXfetch()/XHR
CSRF-EnforcementAutomatisch via ContractManuell, fehleranfällig
KonsistenzDeklarativ im HTMLImperativ in JS verstreut
Server-AutoritätJaClient-State-Chaos möglich
Progressive EnhancementJaNein
DebuggingHTML-Attribute sichtbarDevTools nötig

Contract #14: htmx-patterns

Alle HTMX-Regeln sind in Contract #14 definiert.

Kritische Regeln

IDRegelEnforcement
HTMX-C1hx-post MUSS hx-headers mit X-CSRF-TOKEN habenContract-Validierung
HTMX-C2hx-delete MUSS hx-headers mit X-CSRF-TOKEN habenContract-Validierung
HTMX-C3hx-patch MUSS hx-headers mit X-CSRF-TOKEN habenContract-Validierung
HTMX-C4hx-delete MUSS hx-confirm Attribut habenContract-Validierung
HTMX-C5hx-put MUSS hx-headers mit X-CSRF-TOKEN habenContract-Validierung

Hinweis: Die Regeln werden derzeit durch Contract-Validierung (manuell oder via contracts_validate) geprüft. Ein automatischer Pre-Hook ist geplant aber noch nicht aktiv.

Empfohlene Regeln (WARN)

IDRegelEnforcement
HTMX-R1Kein fetch() in View-DateienContract-Validierung
HTMX-R2hx-indicator für Loading-StatesDokumentation
HTMX-R3hx-swap explizit angebenDokumentation
HTMX-R4hx-disabled-elt für Button-StatesDokumentation

Standard-Patterns

1. Button mit POST-Action

<button hx-post="/api/endpoint"
        hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
        hx-target="#result"
        hx-swap="innerHTML"
        hx-indicator="this"
        hx-disabled-elt="this">
    <span class="htmx-content">Speichern</span>
    <span class="htmx-indicator">Wird gespeichert...</span>
</button>

2. Delete mit Confirmation

<button hx-delete="/api/items/<?= $id ?>"
        hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
        hx-confirm="Wirklich löschen?"
        hx-target="closest tr"
        hx-swap="outerHTML swap:0.3s">
    Löschen
</button>

3. Form mit PUT

<form hx-put="/api/items/<?= $id ?>"
      hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
      hx-target="#form-message"
      hx-swap="innerHTML"
      hx-indicator="#save-btn"
      hx-disabled-elt="#save-btn">
    <!-- Form fields -->
    <button type="submit" id="save-btn">Speichern</button>
</form>
<div id="form-message"></div>

4. Inline-Edit

<input type="text" 
       name="value"
       value="<?= htmlspecialchars($value) ?>"
       hx-post="/api/items/<?= $id ?>/field"
       hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
       hx-trigger="blur, keyup[key=='Enter']"
       hx-swap="none"
       hx-disabled-elt="this"
       hx-on::after-request="this.classList.toggle('is-saved', event.detail.successful)">

5. Select mit Auto-Save

<select name="status"
        hx-post="/api/items/<?= $id ?>/status"
        hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
        hx-swap="none"
        hx-disabled-elt="this">
    <option value="draft">Entwurf</option>
    <option value="active">Aktiv</option>
</select>

Controller-Integration

Basis-Controller Methoden

// In Framework\Controller

// CSRF validieren (wirft 403 bei Fehler)
$this->requireCsrf();

// Erfolg-Alert (via HX-Retarget zu #htmx-messages)
$this->htmxSuccess('Gespeichert!');

// Fehler-Alert
$this->htmxError('Fehler beim Speichern');

// Redirect (via HX-Redirect Header)
$this->htmxRedirect('/items/' . $id);

Beispiel-Controller

public function update(int $id): void
{
    $this->requireCsrf();
    
    try {
        $data = $this->getJsonInput();
        $this->useCase->update($id, $data);
        $this->htmxSuccess('Gespeichert');
    } catch (\Exception $e) {
        $this->htmxError($e->getMessage());
    }
}

Layout-Anforderungen

Globaler Message-Container

In layout.php muss dieser Container existieren:

<main>
    <div id="htmx-messages" class="htmx-messages" aria-live="polite"></div>
    <?= $content ?>
</main>

CSS für HTMX-States

/* Message-Container */
.htmx-messages {
    position: fixed;
    top: 80px;
    right: var(--space-lg);
    z-index: 1000;
    max-width: 400px;
}

/* Loading-States */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline; }
.htmx-request .htmx-content { display: none; }

/* Disabled während Request */
.htmx-request[hx-disabled-elt] { 
    opacity: 0.6; 
    pointer-events: none; 
}

/* Erfolgs-Feedback */
.is-saved {
    border-color: var(--color-success);
    animation: flash-success 0.5s;
}

Enforcement

Contract-Validierung

Die HTMX-Regeln werden via Contract #14 (htmx-patterns) validiert:

# Manuelle Validierung
contracts_validate(name="htmx-patterns")

# Oder via Script
/var/www/scripts/contract-check.sh

Geplant: Pre-Hook

Ein automatischer Pre-Hook (pre_rules_htmx.py) für Write/Edit-Operationen ist geplant, aber noch nicht in der Hook-Konfiguration aktiviert.

Ausnahmen

HTMX ist nicht geeignet für:

Für diese Fälle: Explizite Ausnahme dokumentieren und minimal JS verwenden.

Verwandte Themen

HTMX Patterns Referenz

Erstellt: 2025-12-27 | Aktualisiert: 2025-12-27

Kopierbare Patterns für häufige Anwendungsfälle.

CRUD-Operationen

Create (POST)

<form hx-post="/api/items"
      hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
      hx-target="#form-message"
      hx-swap="innerHTML"
      hx-indicator="#create-btn"
      hx-disabled-elt="#create-btn">
    
    <input type="text" name="title" required>
    <textarea name="description"></textarea>
    
    <button type="submit" id="create-btn" class="btn btn--primary">
        <span class="htmx-content">Erstellen</span>
        <span class="htmx-indicator">Erstelle...</span>
    </button>
</form>
<div id="form-message"></div>

Read (GET mit Partial)

<div hx-get="/api/items/<?= $id ?>/details"
     hx-trigger="revealed"
     hx-swap="innerHTML">
    Lade Details...
</div>

Update (PUT)

<form hx-put="/api/items/<?= $id ?>"
      hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
      hx-target="#form-message"
      hx-swap="innerHTML"
      hx-indicator="#save-btn"
      hx-disabled-elt="#save-btn">
    
    <input type="text" name="title" value="<?= htmlspecialchars($item['title']) ?>">
    
    <button type="submit" id="save-btn" class="btn btn--primary">
        <span class="htmx-content">Speichern</span>
        <span class="htmx-indicator">Speichere...</span>
    </button>
</form>
<div id="form-message"></div>

Delete (DELETE)

<button hx-delete="/api/items/<?= $id ?>"
        hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
        hx-confirm="'<?= htmlspecialchars($item['title']) ?>' wirklich löschen?"
        hx-target="closest tr"
        hx-swap="outerHTML swap:0.3s"
        class="btn btn--danger">
    Löschen
</button>

Inline-Editing

Text-Input mit Auto-Save

<input type="text" 
       name="title"
       value="<?= htmlspecialchars($value) ?>"
       class="inline-edit"
       hx-post="/api/items/<?= $id ?>/title"
       hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
       hx-trigger="blur changed, keyup[key=='Enter'] changed"
       hx-swap="none"
       hx-disabled-elt="this"
       hx-on::after-request="this.classList.toggle('is-saved', event.detail.successful); setTimeout(() => this.classList.remove('is-saved'), 1000)">

Select mit Auto-Save

<select name="status"
        class="inline-select"
        hx-post="/api/items/<?= $id ?>/status"
        hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
        hx-swap="none"
        hx-disabled-elt="this"
        hx-on::after-request="this.classList.toggle('is-saved', event.detail.successful); setTimeout(() => this.classList.remove('is-saved'), 1000)">
    <?php foreach ($statuses as $key => $label): ?>
    <option value="<?= $key ?>" <?= $item['status'] === $key ? 'selected' : '' ?>>
        <?= htmlspecialchars($label) ?>
    </option>
    <?php endforeach; ?>
</select>

Toggle (Checkbox)

<input type="checkbox"
       name="is_active"
       <?= $item['is_active'] ? 'checked' : '' ?>
       hx-post="/api/items/<?= $id ?>/toggle-active"
       hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
       hx-swap="none"
       hx-disabled-elt="this">

Listen & Tabellen

Lazy-Load Tabelle

<table>
    <thead>...</thead>
    <tbody hx-get="/api/items?page=1"
           hx-trigger="revealed"
           hx-swap="innerHTML">
        <tr><td colspan="5">Lade Daten...</td></tr>
    </tbody>
</table>

Infinite Scroll

<div id="items-list">
    <?php foreach ($items as $item): ?>
    <div class="item"><?= htmlspecialchars($item['title']) ?></div>
    <?php endforeach; ?>
    
    <?php if ($hasMore): ?>
    <div hx-get="/api/items?page=<?= $page + 1 ?>"
         hx-trigger="revealed"
         hx-swap="outerHTML"
         hx-indicator="#load-more-spinner">
        <span id="load-more-spinner" class="htmx-indicator">Lade mehr...</span>
    </div>
    <?php endif; ?>
</div>

Sortierbare Spalten

<th hx-get="/api/items?sort=title&dir=<?= $sortDir === 'asc' ? 'desc' : 'asc' ?>"
    hx-target="#items-tbody"
    hx-swap="innerHTML"
    class="sortable <?= $sortCol === 'title' ? 'sorted-' . $sortDir : '' ?>">
    Titel
</th>

Suche & Filter

Live-Search

<input type="search"
       name="q"
       placeholder="Suchen..."
       hx-get="/api/items/search"
       hx-trigger="input changed delay:300ms, search"
       hx-target="#search-results"
       hx-swap="innerHTML"
       hx-indicator="#search-spinner">
<span id="search-spinner" class="htmx-indicator">...</span>
<div id="search-results"></div>

Filter-Form

<form hx-get="/api/items"
      hx-trigger="change from:select, change from:input[type='checkbox']"
      hx-target="#items-list"
      hx-swap="innerHTML"
      hx-indicator="#filter-spinner">
    
    <select name="status">
        <option value="">Alle Status</option>
        <option value="active">Aktiv</option>
        <option value="draft">Entwurf</option>
    </select>
    
    <select name="category">
        <option value="">Alle Kategorien</option>
    </select>
    
    <span id="filter-spinner" class="htmx-indicator">Filtere...</span>
</form>

Modals & Dialogs

Modal laden

<button hx-get="/api/items/<?= $id ?>/edit-modal"
        hx-target="#modal-container"
        hx-swap="innerHTML"
        hx-on::after-request="document.getElementById('modal-container').showModal()">
    Bearbeiten
</button>

<dialog id="modal-container"></dialog>

Modal-Inhalt (Partial)

<form hx-put="/api/items/<?= $id ?>"
      hx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'
      hx-target="#modal-container"
      hx-swap="innerHTML"
      hx-on::after-request="if(event.detail.successful) document.getElementById('modal-container').close()">
    
    <h2>Bearbeiten</h2>
    <input type="text" name="title" value="<?= htmlspecialchars($item['title']) ?>">
    
    <button type="submit">Speichern</button>
    <button type="button" onclick="this.closest('dialog').close()">Abbrechen</button>
</form>

Polling & Updates

Auto-Refresh

<div hx-get="/api/status"
     hx-trigger="every 5s"
     hx-swap="innerHTML">
    Status: <?= $status ?>
</div>

Polling mit Stop-Condition

<div hx-get="/api/jobs/<?= $jobId ?>/status"
     hx-trigger="<?= $job['status'] === 'running' ? 'every 2s' : 'none' ?>"
     hx-swap="outerHTML">
    Status: <?= $job['status'] ?>
    <?php if ($job['status'] === 'running'): ?>
    <span class="spinner"></span>
    <?php endif; ?>
</div>

Verwandte Themen

HTMX Troubleshooting

Erstellt: 2025-12-27 | Aktualisiert: 2025-12-31

Häufige Probleme

1. CSRF-Token Fehler (403)

SymptomRequest schlägt mit 403 fehl
Ursachehx-headers fehlt oder Token ist ungültig
Lösunghx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'

Prüfen:

2. hx-indicator zeigt nichts an

SymptomLoading-Spinner erscheint nicht
Ursachehx-indicator zeigt auf Element, CSS fehlt

Lösung:

<!-- Indicator braucht ID oder Selektor -->
<button hx-indicator="#my-spinner">...</button>
<span id="my-spinner" class="htmx-indicator">Loading...</span>
/* CSS muss vorhanden sein */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline; }

3. Response wird nicht angezeigt

SymptomRequest erfolgreich, aber nichts passiert
Ursachen
  1. hx-target zeigt auf nicht existierendes Element
  2. hx-swap="none" gesetzt
  3. Response ist leer

Debugging:

<!-- Temporär hinzufügen -->
hx-on::after-request="console.log('Response:', event.detail.xhr.responseText)"

4. Button bleibt disabled

SymptomNach Request bleibt Button deaktiviert
Ursachehx-disabled-elt wird nicht zurückgesetzt bei Fehler
LösungHTMX macht das automatisch. Prüfen ob JS den Button manuell disabled.

5. Doppelte Requests

SymptomEin Klick löst mehrere Requests aus
Ursachen
  1. Event-Bubbling (Button in Form)
  2. Mehrere HTMX-Attribute matchen

Lösung:

<!-- Bei Button in Form -->
<button type="button" hx-post="...">  <!-- type="button" statt submit -->

<!-- Oder -->
<form hx-post="..." hx-trigger="submit">
    <button type="submit">Submit</button>  <!-- Nur form hat hx-post -->
</form>

6. Form-Daten werden nicht gesendet

SymptomPOST-Request hat leeren Body
Ursachehx-* auf Button statt Form

Falsch:

<form>
    <input name="title">
    <button hx-post="/api">Send</button>
</form>

Richtig:

<form hx-post="/api">
    <input name="title">
    <button type="submit">Send</button>
</form>

7. History/Back-Button Probleme

SymptomBrowser-Back funktioniert nicht wie erwartet
UrsacheHTMX ersetzt Inhalte ohne History-Push

Lösung:

<!-- Für Navigation, die History braucht -->
<a href="/page" hx-push-url="true" hx-get="/page" hx-target="#main">Link</a>

<!-- Oder: Normaler Link ohne HTMX -->
<a href="/page">Link</a>

Debugging-Tools

1. HTMX Debug-Extension

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

Loggt alle HTMX-Events in die Console.

2. Request/Response in DevTools

  1. DevTools → Network
  2. XHR/Fetch filtern
  3. Request Headers prüfen (X-CSRF-TOKEN, HX-Request)
  4. Response Body prüfen

3. HTMX Events manuell loggen

document.body.addEventListener('htmx:beforeRequest', (e) => {
    console.log('Before:', e.detail);
});

document.body.addEventListener('htmx:afterRequest', (e) => {
    console.log('After:', e.detail);
    console.log('Response:', e.detail.xhr.responseText);
});

document.body.addEventListener('htmx:responseError', (e) => {
    console.error('Error:', e.detail);
});

Contract-Validierung Fehlermeldungen

Bei Contract-Validierung (contracts_validate(name="htmx-patterns")) können folgende Fehler auftreten:

"hx-post missing CSRF token"

BedeutungHTMX-C1 verletzt
Fixhx-headers='{"X-CSRF-TOKEN": "<?= $csrfToken ?>"}'

"hx-delete missing confirmation"

BedeutungHTMX-C4 verletzt
Fixhx-confirm="Wirklich löschen?"

Best Practices

1. Immer explizites Target

<!-- Gut -->
hx-target="#result-container"

<!-- Vermeiden (ersetzt Element selbst) -->
hx-target="this"

2. Expliziter Swap-Modus

<!-- Gut -->
hx-swap="innerHTML"

<!-- Vermeiden (Default ist innerHTML, aber explizit ist klarer) -->
(kein hx-swap)

3. Feedback bei hx-swap="none"

<!-- Bei hx-swap="none" braucht User visuelles Feedback -->
<input hx-swap="none"
       hx-on::after-request="this.classList.add('is-saved'); setTimeout(() => this.classList.remove('is-saved'), 1000)">

Verwandte Themen