Files
nebula/HTMX.md

571 lines
13 KiB
Markdown
Raw Normal View History

# HTMX 4 Integration Guide for Nebula
This document details htmx 4 enhancements and patterns optimized for our Go/Templ server-driven architecture.
## Why htmx 4?
htmx 4 introduces significant architectural improvements that align perfectly with Go-based server rendering:
- **Built-in SSE streaming** - Native support for `text/event-stream` responses
- **Morphing swaps** - Preserve DOM state during updates (forms, video, focus)
- **Partial tags** - Explicit multi-target updates in single responses
- **View Transitions API** - Smooth animated page transitions
- **Simplified extensions** - Page-wide, event-based, zero performance penalty
- **Enhanced request headers** - Better server-side request detection
---
## Installation
```html
<!-- htmx 4 core -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js"></script>
<!-- Extensions (bundled with htmx 4) -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/ext/hx-preload.js"></script>
```
**Note:** htmx 4 extensions are bundled in the htmx.org package at `/dist/ext/`. Do NOT use the separate `htmx-ext-*` packages which use the deprecated `defineExtension` API.
---
## Core Patterns for Go/Templ
### 1. Partial Updates with `<hx-partial>`
The `<hx-partial>` tag enables multiple targeted updates in a single response - perfect for Go handlers that need to update several page sections.
**Go Handler:**
```go
func handleAddToCart(w http.ResponseWriter, r *http.Request) {
// Return multiple partials in one response
components.CartPartials(cartData).Render(r.Context(), w)
}
```
**Templ Component:**
```templ
templ CartPartials(data CartData) {
<hx-partial hx-target="#cart-count" hx-swap="innerHTML">
<span>{ strconv.Itoa(data.Count) }</span>
</hx-partial>
<hx-partial hx-target="#cart-items" hx-swap="beforeend">
@CartItem(data.NewItem)
</hx-partial>
<hx-partial hx-target="#cart-total" hx-swap="innerHTML">
<span>{ data.FormattedTotal }</span>
</hx-partial>
}
```
**HTML Trigger:**
```html
<button hx-post="/cart/add" hx-vals='{"product_id": "123"}'>
Add to Cart
</button>
```
### 2. Morphing Swaps (Preserve State)
Use `innerMorph` or `outerMorph` to update content while preserving:
- Form input values
- Focus state
- Video/audio playback
- Scroll position
**Use Cases:**
```html
<!-- Live search that preserves input focus -->
<input type="search"
hx-get="/search"
hx-trigger="input delay:300ms"
hx-target="#results"
hx-swap="innerMorph">
<!-- Update list without losing scroll position -->
<div hx-get="/notifications"
hx-trigger="every 30s"
hx-swap="outerMorph">
<!-- notifications list -->
</div>
<!-- Dashboard that preserves playing media -->
<div id="dashboard"
hx-get="/dashboard/refresh"
hx-trigger="every 60s"
hx-swap="innerMorph">
</div>
```
**Morph Control Attributes:**
```html
<!-- Skip morphing specific elements -->
<div hx-morph-skip>Never touch this</div>
<!-- Skip children but morph the element itself -->
<div hx-morph-skip-children>
<video><!-- preserved --></video>
</div>
<!-- Ignore during morph comparisons -->
<div hx-morph-ignore>Ephemeral content</div>
```
### 3. View Transitions
Enable smooth animated transitions between page states.
**Global Enable:**
```javascript
htmx.config.transitions = true;
```
**Per-Element:**
```html
<button hx-get="/page" hx-swap="innerHTML transition:true">
Navigate
</button>
<!-- With boosted links -->
<nav hx-boost="transition:true">
<a href="/dashboard">Dashboard</a>
<a href="/settings">Settings</a>
</nav>
```
**CSS Customization:**
```css
::view-transition-group(*) {
animation-duration: 150ms;
}
::view-transition-old(root) {
animation: fade-out 150ms ease-out;
}
::view-transition-new(root) {
animation: fade-in 150ms ease-in;
}
```
### 4. Streaming Responses (SSE)
htmx 4 has native SSE support - no extension needed.
**Go Handler:**
```go
func handleProgress(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
flusher := w.(http.Flusher)
for progress := 0; progress <= 100; progress += 10 {
// Send HTML fragment
fmt.Fprintf(w, "data: <div class=\"progress\" style=\"width: %d%%\"></div>\n\n", progress)
flusher.Flush()
time.Sleep(500 * time.Millisecond)
}
// Final update
fmt.Fprintf(w, "data: <div class=\"complete\">Done!</div>\n\n")
}
```
**HTML:**
```html
<button hx-get="/progress" hx-target="#progress-bar">
Start Processing
</button>
<div id="progress-bar"></div>
```
**Custom Events in SSE:**
```go
// Server sends custom event
fmt.Fprintf(w, "event: notification\n")
fmt.Fprintf(w, "data: {\"count\": 5}\n\n")
```
```html
<!-- Client handles custom event -->
<div hx-get="/stream"
hx-on:notification="updateBadge(event.detail.data)">
</div>
```
---
## Request Detection in Go
### htmx 4 Request Headers
| Header | Description | Example Value |
|--------|-------------|---------------|
| `HX-Request` | Always "true" for htmx requests | `true` |
| `HX-Request-Type` | "partial" or "full" | `partial` |
| `HX-Target` | Target element selector | `#results` |
| `HX-Trigger` | Triggering element ID | `search-input` |
| `HX-Boosted` | Request from boosted element | `true` |
| `HX-Current-URL` | Current page URL | `/dashboard` |
**Go Middleware/Helper:**
```go
func IsHTMXRequest(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}
func IsPartialRequest(r *http.Request) bool {
return r.Header.Get("HX-Request-Type") == "partial"
}
func GetHTMXTarget(r *http.Request) string {
return r.Header.Get("HX-Target")
}
```
**Pattern: Full Page vs Partial Response:**
```go
func handleDashboard(w http.ResponseWriter, r *http.Request) {
data := getDashboardData()
if IsHTMXRequest(r) && IsPartialRequest(r) {
// Return only the content fragment
views.DashboardContent(data).Render(r.Context(), w)
return
}
// Return full page with layout
views.DashboardPage(data).Render(r.Context(), w)
}
```
---
## Response Headers from Go
Control client behavior via response headers:
```go
func handleAction(w http.ResponseWriter, r *http.Request) {
// Redirect client-side (no full page reload)
w.Header().Set("HX-Location", "/new-page")
// Push URL to browser history
w.Header().Set("HX-Push-Url", "/updated-url")
// Replace current URL (no history entry)
w.Header().Set("HX-Replace-Url", "/current")
// Trigger client-side events
w.Header().Set("HX-Trigger", "itemAdded")
w.Header().Set("HX-Trigger-After-Swap", "refreshCart")
// Override swap behavior
w.Header().Set("HX-Reswap", "outerHTML")
w.Header().Set("HX-Retarget", "#different-target")
// Force full page refresh
w.Header().Set("HX-Refresh", "true")
}
```
---
## Attribute Inheritance
htmx 4 uses explicit inheritance with the `:inherited` modifier.
**Pattern: Shared Confirmation:**
```html
<div hx-confirm:inherited="Are you sure?">
<button hx-delete="/item/1">Delete</button>
<button hx-delete="/item/2">Delete</button>
<button hx-delete="/item/3">Delete</button>
</div>
```
**Pattern: Shared Target:**
```html
<div hx-target:inherited="#results" hx-swap:inherited="innerHTML">
<button hx-get="/page/1">Page 1</button>
<button hx-get="/page/2">Page 2</button>
<button hx-get="/page/3">Page 3</button>
</div>
```
**Enable Implicit Inheritance (htmx 2 behavior):**
```javascript
htmx.config.implicitInheritance = true;
```
---
## Advanced Triggers
### Trigger Modifiers
```html
<!-- Debounce input -->
<input hx-get="/search"
hx-trigger="input delay:500ms"
hx-target="#results">
<!-- Throttle scroll -->
<div hx-get="/load-more"
hx-trigger="scroll throttle:1s"
hx-target="#items">
<!-- Once only -->
<div hx-get="/init" hx-trigger="load once">
<!-- Changed value only -->
<select hx-get="/filter" hx-trigger="change changed">
<!-- From another element -->
<button hx-get="/refresh" hx-trigger="click from:#other-button">
```
### Special Events
```html
<!-- Load when element enters DOM -->
<div hx-get="/content" hx-trigger="load">
<!-- Load when scrolled into view -->
<div hx-get="/lazy-content" hx-trigger="revealed">
<!-- Intersection observer with threshold -->
<div hx-get="/analytics"
hx-trigger="intersect threshold:0.5">
<!-- Polling -->
<div hx-get="/status" hx-trigger="every 5s">
<!-- Load polling (self-replacing) -->
<div hx-get="/progress"
hx-trigger="load delay:1s"
hx-swap="outerHTML">
```
---
## Event Handling with `hx-on`
Inline event handlers without separate JavaScript:
```html
<!-- After request completes -->
<form hx-post="/submit"
hx-on:htmx:after:request="this.reset()">
<!-- Before request starts -->
<button hx-post="/action"
hx-on:htmx:before:request="showSpinner()">
<!-- On successful swap -->
<div hx-get="/content"
hx-on:htmx:after:swap="initComponents()">
<!-- Custom SSE events -->
<div hx-get="/stream"
hx-on:progress="updateBar(event.detail.data)">
```
**Using `find()` helper:**
```html
<button hx-post="/save"
hx-on:htmx:after:request="find('closest form').reset()">
Save
</button>
```
---
## Request Synchronization
Prevent race conditions with `hx-sync`:
```html
<!-- Form submission aborts pending validation -->
<form hx-post="/submit" hx-sync="this:replace">
<input hx-post="/validate"
hx-trigger="change"
hx-sync="closest form">
<button type="submit">Submit</button>
</form>
<!-- Queue requests -->
<button hx-post="/action" hx-sync="this:queue">
<!-- Drop new requests while one is pending -->
<button hx-post="/action" hx-sync="this:drop">
<!-- Abort pending, start new -->
<button hx-post="/action" hx-sync="this:abort">
```
**Programmatic Abort:**
```html
<button id="action-btn" hx-post="/long-action">Start</button>
<button onclick="htmx.trigger('#action-btn', 'htmx:abort')">Cancel</button>
```
---
## Loading Indicators
```html
<!-- Inline indicator -->
<button hx-get="/data">
Load Data
<span class="htmx-indicator">Loading...</span>
</button>
<!-- External indicator -->
<button hx-get="/data" hx-indicator="#spinner">
Load Data
</button>
<div id="spinner" class="htmx-indicator">
<wa-spinner></wa-spinner>
</div>
<!-- Disable during request -->
<button hx-get="/data" hx-disabled-elt="this">
Load (disables while loading)
</button>
<!-- Disable multiple elements -->
<form hx-post="/submit" hx-disabled-elt="find input, find button">
```
**CSS for indicators:**
```css
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
opacity: 1;
}
```
---
## Boosting & Navigation
Convert standard links/forms to AJAX:
```html
<!-- Boost all links in nav -->
<nav hx-boost:inherited="true">
<a href="/dashboard">Dashboard</a>
<a href="/settings">Settings</a>
</nav>
<!-- With history support -->
<a href="/page" hx-boost="true" hx-push-url="true">
Navigate with History
</a>
<!-- Boost with transitions -->
<main hx-boost="transition:true">
<a href="/other">Smooth Navigate</a>
</main>
```
---
## Preload Extension
Preload content on hover/mousedown for instant navigation:
```html
<!-- Preload on mousedown (50ms head start) -->
<a href="/page" preload="mousedown">Fast Link</a>
<!-- Preload on hover -->
<a href="/page" preload>Preloaded Link</a>
<!-- Preload with custom trigger -->
<button hx-get="/content" preload="mousedown">
Load Content
</button>
```
---
## Confirmation Dialogs
```html
<!-- Simple confirm -->
<button hx-delete="/item" hx-confirm="Delete this item?">
Delete
</button>
<!-- Custom confirm with JavaScript -->
<button hx-delete="/account"
hx-confirm="js:confirmDelete()">
Delete Account
</button>
<script>
async function confirmDelete() {
const result = await showCustomDialog({
title: 'Delete Account?',
message: 'This cannot be undone.',
confirmText: 'Delete',
variant: 'danger'
});
return result.confirmed;
}
</script>
```
---
## Best Practices for Go/Templ
### 1. Use Partials for Multi-Target Updates
Instead of multiple requests or complex OOB swaps, return `<hx-partial>` tags.
### 2. Leverage Morphing for Stateful Components
Use `innerMorph`/`outerMorph` for forms, media players, and interactive widgets.
### 3. Detect Request Type Server-Side
Return minimal HTML fragments for partial requests, full pages for direct navigation.
### 4. Use Response Headers for Side Effects
Trigger client events, update URLs, and control swapping via headers.
### 5. Preload Critical Paths
Add `preload="mousedown"` to navigation links for perceived instant loading.
### 6. Stream Long Operations
Use SSE (`text/event-stream`) for progress indicators and real-time updates.
---
## Configuration Reference
```javascript
// Enable all htmx 4 features
htmx.config.transitions = true; // View Transitions API
htmx.config.implicitInheritance = false; // Explicit :inherited (default)
htmx.config.selfRequestsOnly = true; // Security: same-origin only
htmx.config.timeout = 0; // Request timeout (0 = none)
htmx.config.historyCacheSize = 10; // History cache entries
```
**Via Meta Tag:**
```html
<meta name="htmx-config" content='{
"transitions": true,
"selfRequestsOnly": true
}'>
```