# HTMX Learning Guide **Last Updated**: December 2024 ## Overview This document explains HTMX patterns used in this CV website project, with practical examples from the codebase. Use this as a learning resource for understanding HTMX concepts. ## Table of Contents 1. [Core Concepts](#core-concepts) 2. [Out-of-Band Swaps (OOB)](#out-of-band-swaps-oob) 3. [Language Switch Pattern](#language-switch-pattern) 4. [Toggle Patterns](#toggle-patterns) 5. [Contact Form Pattern](#contact-form-pattern) 6. [Skeleton Loaders](#skeleton-loaders) 7. [HTML Invoker Commands API](#html-invoker-commands-api) 8. [Lazy Loading Web Components](#lazy-loading-web-components) 9. [Common Attributes Reference](#common-attributes-reference) --- ## Core Concepts ### What is HTMX? HTMX allows you to build modern user interfaces with simple HTML attributes instead of JavaScript. It extends HTML with attributes that enable: - **AJAX requests** from any element (not just links/forms) - **Partial page updates** without full page reloads - **CSS transitions** on swaps - **WebSocket/SSE** support ### Basic Example ```html
``` --- ## Out-of-Band Swaps (OOB) ### The Problem Normal HTMX swaps can only update ONE target element. But what if you need to update MULTIPLE elements with a single request? **Example**: When switching languages, we need to update: - Language selector buttons (show which is active) - Page 1 content (header, experience, education) - Page 2 content (awards, projects, courses) - Footer ### The Solution: `hx-swap-oob` OOB (Out-of-Band) swaps let you update ANY element on the page by including it in your response with a matching `id`. ```html
Main content here
5 new messages
``` ### OOB Swap Types | Attribute | Effect | |-----------|--------| | `hx-swap-oob="true"` | Replace inner HTML | | `hx-swap-oob="innerHTML"` | Replace inner HTML | | `hx-swap-oob="outerHTML"` | Replace entire element including itself | | `hx-swap-oob="beforebegin"` | Insert before element | | `hx-swap-oob="afterend"` | Insert after element | --- ## Language Switch Pattern **File**: `templates/language-switch.html` This is the most complex HTMX pattern in the project - updating the entire CV content when switching languages. ### How It Works 1. **User clicks language button** (EN or ES) 2. **HTMX sends request** to `/switch-language?lang=es` 3. **Server renders new content** for both pages in the selected language 4. **OOB swaps update** both page containers atomically ### The Template Structure ```html
{{template "title-badges" .}}
{{template "section-header" .}} {{template "section-education" .}} {{template "section-experience" .}}
{{template "title-badges" .}}
{{template "section-awards" .}} {{template "section-projects" .}} {{template "section-courses" .}}
{{template "cv-footer" .}}
``` ### Why Two Separate Page Divs? The CV is designed as a **2-page printable document**: | Page 1 | Page 2 | |--------|--------| | Header | Awards | | Education | Projects | | Skills Summary | Courses | | Experience | Languages | | Left Sidebar (Skills) | References | | | Right Sidebar (More Skills) | | | Footer | Each page has its own layout grid and sidebar positioning, so they need separate containers. ### The Hyperscript Integration ```html _="on htmx:afterSettle wait 100ms then remove .loading from me" ``` This hyperscript: 1. Listens for `htmx:afterSettle` event (content fully swapped) 2. Waits 100ms (for CSS transitions) 3. Removes `.loading` class (hides skeleton, shows content) ### Visual Flow ``` ┌─────────────────────────────────────────────────────────┐ │ User clicks "ES" │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ hx-post="/switch-language?lang=es" │ │ hx-target="#cv-content" │ │ hx-swap="none" ← Response body ignored for main swap │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Server Response Contains: │ │ │ │ 1. Language selector (hx-swap-oob="outerHTML") │ │ 2. Page 1 content (hx-swap-oob="outerHTML") │ │ 3. Page 2 content (hx-swap-oob="outerHTML") │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ HTMX matches IDs and swaps all three simultaneously │ │ → Atomic update, no flicker │ └─────────────────────────────────────────────────────────┘ ``` --- ## Toggle Patterns ### Pattern: `hx-swap="none"` + Cookies Toggles (length, theme, icons) don't need server-rendered content because: 1. The server just needs to **set a cookie** 2. The frontend uses **hyperscript** to toggle UI state ```html ``` ### Server Handler ```go // Returns 204 No Content - body is ignored anyway func (h *CVHandler) ToggleLength(w http.ResponseWriter, r *http.Request) { // Toggle cookie value prefs := middleware.GetPreferences(r) newLength := "long" if prefs.CVLength == "long" { newLength = "short" } // Set cookie middleware.SetPreferenceCookie(w, "cv-length", newLength) // Return 204 - no body needed w.WriteHeader(http.StatusNoContent) } ``` ### Why This Works 1. `hx-swap="none"` tells HTMX to **ignore the response body** 2. The **cookie gets set** (browser handles this automatically) 3. **Hyperscript handles UI** changes locally 4. On **next page load**, server reads cookie and renders correctly --- ## Contact Form Pattern **File**: `templates/partials/contact/contact-form.html` ### Pattern: `hx-target` + Partial Replacement ```html
``` ### Server Responses **Success**: Returns success partial ```html

Message sent successfully!

``` **Error**: Returns error partial ```html

{{.ErrorMessage}}

``` ### The `hx-indicator` Pattern ```html hx-indicator="#contact-spinner" ``` HTMX automatically: 1. Adds `.htmx-request` class to indicator during request 2. Shows spinner via CSS: `.htmx-indicator { display: none } .htmx-request .htmx-indicator { display: inline }` --- ## Skeleton Loaders ### Pattern: CSS Class Toggle with Hyperscript The skeleton loader system uses a **dual-state structure**: ```html

John Smith

Software Engineer

``` ### CSS State Control ```css /* Default: Show content, hide skeleton */ .component-wrapper .actual-content { opacity: 1; } .component-wrapper .skeleton-content { opacity: 0; pointer-events: none; } /* Loading: Hide content, show skeleton */ .component-wrapper.loading .actual-content { opacity: 0; } .component-wrapper.loading .skeleton-content { opacity: 1; } ``` ### Triggering Loading State Before language switch: ```javascript // Add .loading to show skeletons document.querySelectorAll('.cv-page-content-wrapper').forEach(el => { el.classList.add('loading'); }); ``` After content loads (via hyperscript): ```html _="on htmx:afterSettle wait 100ms then remove .loading from me" ``` --- ## HTML Invoker Commands API **Browser Support**: Chrome/Edge 135+, Firefox Nightly, Safari TP ### The Problem Opening and closing `` elements traditionally requires JavaScript: ```html ``` This is verbose, error-prone, and mixes behavior with markup. ### The Solution: `commandfor` + `command` The new HTML Invoker Commands API provides declarative modal control: ```html ``` ### Command Values | Command | Effect | Target Element | |---------|--------|----------------| | `show-modal` | Opens dialog as modal | `` | | `close` | Closes dialog | `` | | `show-popover` | Shows popover | `[popover]` | | `hide-popover` | Hides popover | `[popover]` | | `toggle-popover` | Toggles popover | `[popover]` | ### Project Implementation **Files**: `templates/partials/widgets/*.html`, `templates/partials/modals/*.html` ```html
``` ### Benefits 1. **No JavaScript** - Pure HTML declarative syntax 2. **Accessibility** - Built-in keyboard and screen reader support 3. **Reduced Errors** - No typos in element IDs within JavaScript 4. **Cleaner Templates** - Removes onclick clutter 5. **Progressive Enhancement** - Graceful degradation in older browsers ### Fallback for Older Browsers If you need to support browsers without Invoker Commands: ```html ``` --- ## Lazy Loading Web Components ### The Problem Heavy web components (like ninja-keys command palette) add significant initial load time even when users may never use them: ``` Initial Load: 81 module requests, ~300KB, 2+ seconds ``` ### The Solution: Dynamic Import on Demand Only load the component when the user actually needs it: ```javascript // Don't import at top of file // import 'ninja-keys'; // ❌ Loads immediately // Instead, lazy load on first use let loaded = false; async function loadNinjaKeys() { if (loaded) return; // Dynamic import - only fetches when called await import('https://esm.sh/ninja-keys@1.2.2?bundle'); // Create element after module loads const container = document.getElementById('cmd-k-container'); const ninjaKeys = document.createElement('ninja-keys'); ninjaKeys.id = 'cmd-k-bar'; container.appendChild(ninjaKeys); loaded = true; } // Trigger on user action document.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); loadNinjaKeys(); } }); ``` ### Project Implementation **File**: `templates/partials/layout/body-scripts.html` ```html
``` ### CDN Choice: esm.sh with ?bundle | CDN | Requests | Why | |-----|----------|-----| | unpkg.com | 80+ (redirect chains) | ❌ Follows all peer deps | | esm.sh | 80+ (without bundle) | ❌ Resolves all imports | | esm.sh?bundle | 2-3 | ✅ Pre-bundled single file | | jsdelivr | 1 | ✅ Also good option | ```javascript // ❌ Triggers 80+ module requests await import('https://esm.sh/ninja-keys@1.2.2'); // ✅ Single bundled file await import('https://esm.sh/ninja-keys@1.2.2?bundle'); ``` ### CSP Configuration Remember to add your CDN to Content Security Policy: ```go // internal/middleware/security.go csp := "default-src 'self'; " + "script-src 'self' 'unsafe-inline' https://esm.sh https://cdn.jsdelivr.net; " + // ... ``` ### Performance Results | Metric | Before | After | |--------|--------|-------| | Initial requests | 81 | 0 | | Initial load time | +2.1s | 0ms | | On CMD+K | 0 | 3 requests | | Subsequent uses | 0 | 0 (cached) | ### Best Practices 1. **Use Placeholder Containers** - Empty div ready for component injection 2. **Prevent Double Loading** - Track loading state with flags 3. **Bundle Dependencies** - Use `?bundle` parameter on esm.sh 4. **Cache First Load** - Browser caches subsequent uses automatically 5. **Multiple Triggers** - Support keyboard AND button triggers 6. **Initialization Delay** - Wait briefly after element creation for setup --- ## Common Attributes Reference ### Request Attributes | Attribute | Description | Example | |-----------|-------------|---------| | `hx-get` | GET request | `hx-get="/api/data"` | | `hx-post` | POST request | `hx-post="/api/submit"` | | `hx-put` | PUT request | `hx-put="/api/update/1"` | | `hx-delete` | DELETE request | `hx-delete="/api/item/1"` | ### Target & Swap Attributes | Attribute | Description | Example | |-----------|-------------|---------| | `hx-target` | Element to update | `hx-target="#results"` | | `hx-swap` | How to swap content | `hx-swap="innerHTML"` | | `hx-swap-oob` | Out-of-band swap | `hx-swap-oob="outerHTML"` | ### Swap Values | Value | Effect | |-------|--------| | `innerHTML` | Replace inner HTML (default) | | `outerHTML` | Replace entire element | | `beforebegin` | Insert before element | | `afterbegin` | Insert at start of element | | `beforeend` | Insert at end of element | | `afterend` | Insert after element | | `none` | Don't swap (use for side effects only) | ### Trigger Attributes | Attribute | Description | Example | |-----------|-------------|---------| | `hx-trigger` | Event to trigger request | `hx-trigger="click"` | | `hx-indicator` | Loading indicator | `hx-indicator="#spinner"` | | `hx-confirm` | Confirmation dialog | `hx-confirm="Are you sure?"` | ### Special Triggers ```html hx-trigger="input changed delay:500ms" hx-trigger="revealed" hx-trigger="load" hx-trigger="intersect" ``` --- ## Best Practices Learned ### 1. Use OOB for Multi-Element Updates Instead of multiple requests, use OOB swaps: ```html
Updated main
New notification
``` ### 2. Use `hx-swap="none"` for Side Effects When you only need server-side effects (cookies, database), skip the swap: ```html ``` ```css .htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; } ``` ### 5. Return Proper HTTP Status Codes - `200 OK` - Success with content - `204 No Content` - Success, no body needed - `422 Unprocessable Entity` - Validation errors - HTMX handles these gracefully --- ## Further Reading - [HTMX Documentation](https://htmx.org/docs/) - [Hyperscript Documentation](https://hyperscript.org/docs/) - [HTMX Examples](https://htmx.org/examples/) - Project doc: `doc/2-MODERN-WEB-TECHNIQUES.md` - Full techniques reference - Project doc: `doc/4-HYPERSCRIPT-RULES.md` - Hyperscript patterns